wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ExerciseTourResources.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <algorithm>
7
8#include <QDebug>
9#include <QDir>
10#include <QFile>
11#include <QFileInfo>
12#include <QJsonDocument>
13#include <QJsonObject>
14#include <QJsonValue>
15#include <QStandardPaths>
16
18#include "App/Core/Settings.h"
19
20QVector<ExerciseTourResourceEntry> ExerciseTourResources::scan(const QString &directory)
21{
22 QVector<ExerciseTourResourceEntry> entries;
23
24 QDir dir(directory);
25 if (!dir.exists()) {
26 qWarning() << "ExerciseTourResources::scan: directory not found:" << directory;
27 return entries;
28 }
29
30 const QStringList jsonFiles = dir.entryList({QStringLiteral("*.json")}, QDir::Files);
31 for (const QString &fileName : jsonFiles) {
32 const QString path = directory + QLatin1Char('/') + fileName;
33
34 QFile file(path);
35 if (!file.open(QIODevice::ReadOnly)) {
36 qWarning() << "ExerciseTourResources::scan: failed to open" << path;
37 continue;
38 }
39
40 const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
41 if (doc.isNull() || !doc.isObject()) {
42 qWarning() << "ExerciseTourResources::scan: malformed JSON in" << path;
43 continue;
44 }
45
46 const QJsonObject root = doc.object();
47 const QString id = root.value(QStringLiteral("id")).toString();
48 const QString title = root.value(QStringLiteral("title")).toString();
49 if (id.isEmpty() || title.isEmpty()) {
50 qWarning() << "ExerciseTourResources::scan: missing id/title in" << path;
51 continue;
52 }
53
54 entries.append({path, id, title, root.value(QStringLiteral("description")).toString()});
55 }
56
57 std::sort(entries.begin(), entries.end(), [](const ExerciseTourResourceEntry &a, const ExerciseTourResourceEntry &b) {
58 return a.id < b.id;
59 });
60
61 return entries;
62}
63
64QVector<ExerciseTourResourceEntry> ExerciseTourResources::mergeUnique(QVector<ExerciseTourResourceEntry> base,
65 const QVector<ExerciseTourResourceEntry> &additional)
66{
67 for (const ExerciseTourResourceEntry &entry : additional) {
68 const bool alreadyPresent = std::any_of(base.cbegin(), base.cend(), [&entry](const ExerciseTourResourceEntry &existing) {
69 return existing.id == entry.id;
70 });
71 if (alreadyPresent) {
72 qWarning() << "ExerciseTourResources::mergeUnique: skipping duplicate id" << entry.id << "at" << entry.path;
73 continue;
74 }
75 base.append(entry);
76 }
77
78 std::sort(base.begin(), base.end(), [](const ExerciseTourResourceEntry &a, const ExerciseTourResourceEntry &b) {
79 return a.id < b.id;
80 });
81
82 return base;
83}
84
85QString ExerciseTourResources::managedContentDir(const QString &category)
86{
87 const QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1Char('/') + category;
88 QDir dir(path);
89 if (!dir.exists()) {
90 dir.mkpath(path);
91 }
92 return path;
93}
94
95QString ExerciseTourResources::documentsFallbackDir(const QString &category)
96{
97 return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + QStringLiteral("/wiRedPanda/") + category;
98}
99
100QString ExerciseTourResources::resolveWritableDir(const QStringList &candidates, const QString &documentsFallback)
101{
102 for (const QString &candidate : candidates) {
103 if (candidate.isEmpty()) {
104 continue;
105 }
106 QDir dir(candidate);
107 if ((dir.exists() || dir.mkpath(candidate)) && QFileInfo(candidate).isWritable()) {
108 return candidate;
109 }
110 }
111
112 QDir dir(documentsFallback);
113 if ((dir.exists() || dir.mkpath(documentsFallback)) && QFileInfo(documentsFallback).isWritable()) {
114 return documentsFallback;
115 }
116
117 return {};
118}
119
120QString ExerciseTourResources::preferredContentDir(const QString &category)
121{
122 return resolveWritableDir(InstallRelativePaths::candidates(category), documentsFallbackDir(category));
123}
124
125QVector<ExerciseTourResourceEntry> ExerciseTourResources::discover(const QString &category)
126{
127 QVector<ExerciseTourResourceEntry> entries = scan(QStringLiteral(":/") + category);
128
129 entries = mergeUnique(entries, scan(managedContentDir(category)));
130
131 const QString installDir = InstallRelativePaths::resolve(category);
132 if (!installDir.isEmpty()) {
133 entries = mergeUnique(entries, scan(installDir));
134 }
135
136 const QString docsDir = documentsFallbackDir(category);
137 if (QDir(docsDir).exists()) {
138 entries = mergeUnique(entries, scan(docsDir));
139 }
140
141 return entries;
142}
143
144QString ExerciseTourResources::translateFromCatalog(const QString &catalogPath, const QString &key,
145 const QString &fallbackEnglish)
146{
147 QFile file(catalogPath);
148 if (!file.open(QIODevice::ReadOnly)) {
149 return fallbackEnglish;
150 }
151
152 const QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
153 if (!doc.isObject()) {
154 return fallbackEnglish;
155 }
156
157 QJsonValue node = doc.object();
158 const QStringList parts = key.split(QLatin1Char('.'));
159 for (const QString &part : parts) {
160 if (!node.isObject()) {
161 return fallbackEnglish;
162 }
163 node = node.toObject().value(part);
164 }
165
166 return (node.isString() && !node.toString().isEmpty()) ? node.toString() : fallbackEnglish;
167}
168
169QString ExerciseTourResources::translate(const QString &key, const QString &fallbackEnglish)
170{
171 const QString lang = Settings::language();
172 if (lang.isEmpty() || lang == QLatin1String("en")) {
173 return fallbackEnglish;
174 }
175
176 return translateFromCatalog(QStringLiteral(":/i18n/ExerciseTour/%1.json").arg(lang), key, fallbackEnglish);
177}
Discovery and translation lookup for Exercise/Tour step content.
Per-platform resolution of install-relative content directories.
Typed wrappers around QSettings for all application preferences.
static QVector< ExerciseTourResourceEntry > discover(const QString &category)
static QVector< ExerciseTourResourceEntry > mergeUnique(QVector< ExerciseTourResourceEntry > base, const QVector< ExerciseTourResourceEntry > &additional)
static QString preferredContentDir(const QString &category)
static QVector< ExerciseTourResourceEntry > scan(const QString &directory)
static QString translateFromCatalog(const QString &catalogPath, const QString &key, const QString &fallbackEnglish)
static QString managedContentDir(const QString &category)
static QString resolveWritableDir(const QStringList &candidates, const QString &documentsFallback)
static QString translate(const QString &key, const QString &fallbackEnglish)
static QString resolve(const QString &category)
static QStringList candidates(const QString &category)
static QString language()
Definition Settings.cpp:142