|
1 | 1 | #include "extension.h"
|
2 | 2 |
|
| 3 | +#include <QDirIterator> |
3 | 4 | #include <QFile>
|
4 | 5 | #include <QFileInfo>
|
| 6 | +#include <QJsonDocument> |
| 7 | + |
| 8 | +#include "log.h" |
5 | 9 |
|
6 | 10 | namespace MOBase
|
7 | 11 | {
|
8 | 12 |
|
| 13 | +ExtensionMetaData::ExtensionMetaData(QJsonObject const& jsonData) : m_JsonData{jsonData} |
| 14 | +{ |
| 15 | + // read basic fields |
| 16 | + m_Identifier = jsonData["identifier"].toString(); |
| 17 | + m_Type = parseType(jsonData["type"].toString()); |
| 18 | + m_Name = jsonData["name"].toString(); |
| 19 | + m_Description = jsonData["description"].toString(); |
| 20 | + m_Version.parse(jsonData["version"].toString("0.0.0")); |
| 21 | + |
| 22 | + // TODO: name of the key |
| 23 | + // translation context |
| 24 | + m_TranslationContext = jsonData["translationContext"].toString(""); |
| 25 | +} |
| 26 | + |
| 27 | +bool ExtensionMetaData::isValid() const |
| 28 | +{ |
| 29 | + return !m_Identifier.isEmpty() && !m_Name.isEmpty() && m_Version.isValid() && |
| 30 | + m_Type != ExtensionType::INVALID; |
| 31 | +} |
| 32 | + |
| 33 | +ExtensionType ExtensionMetaData::parseType(QString const& value) const |
| 34 | +{ |
| 35 | + std::map<QString, ExtensionType> stringToTypes{ |
| 36 | + {"theme", ExtensionType::THEME}, |
| 37 | + {"translation", ExtensionType::TRANSLATION}, |
| 38 | + {"plugin", ExtensionType::PLUGIN}, |
| 39 | + {"game", ExtensionType::GAME}}; |
| 40 | + |
| 41 | + auto type = ExtensionType::INVALID; |
| 42 | + for (auto& [k, v] : stringToTypes) { |
| 43 | + if (k.compare(value, Qt::CaseInsensitive) == 0) { |
| 44 | + type = v; |
| 45 | + break; |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + return type; |
| 50 | +} |
| 51 | + |
| 52 | +QString ExtensionMetaData::localized(QString const& value) const |
| 53 | +{ |
| 54 | + // no translation context |
| 55 | + if (m_TranslationContext.isEmpty()) { |
| 56 | + return value; |
| 57 | + } |
| 58 | + |
| 59 | + const auto result = QCoreApplication::translate(m_TranslationContext.toUtf8().data(), |
| 60 | + value.toUtf8().data()); |
| 61 | + return result.isEmpty() ? value : result; |
| 62 | +} |
| 63 | + |
9 | 64 | IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata)
|
10 | 65 | : m_Path{std::move(path)}, m_MetaData{std::move(metadata)}
|
11 | 66 | {}
|
12 | 67 |
|
13 |
| -std::vector<QObject*> IExtension::plugins() const |
| 68 | +std::unique_ptr<IExtension> |
| 69 | +ExtensionFactory::loadExtension(std::filesystem::path directory) |
| 70 | +{ |
| 71 | + const auto metadataPath = directory / METADATA_FILENAME; |
| 72 | + |
| 73 | + if (!exists(metadataPath)) { |
| 74 | + log::warn("missing extension metadata in '{}'", directory.native()); |
| 75 | + return nullptr; |
| 76 | + } |
| 77 | + |
| 78 | + // load the meta data |
| 79 | + QJsonParseError jsonError; |
| 80 | + QJsonDocument jsonMetaData; |
| 81 | + { |
| 82 | + QFile file(metadataPath); |
| 83 | + if (!file.open(QFile::ReadOnly)) { |
| 84 | + return {}; |
| 85 | + } |
| 86 | + |
| 87 | + const auto jsonContent = file.readAll(); |
| 88 | + jsonMetaData = QJsonDocument::fromJson(jsonContent, &jsonError); |
| 89 | + } |
| 90 | + |
| 91 | + if (jsonMetaData.isNull()) { |
| 92 | + log::warn("failed to read metadata from '{}': {}", metadataPath.native(), |
| 93 | + jsonError.errorString()); |
| 94 | + return nullptr; |
| 95 | + } |
| 96 | + |
| 97 | + return loadExtension(std::move(directory), ExtensionMetaData(jsonMetaData.object())); |
| 98 | +} |
| 99 | + |
| 100 | +std::unique_ptr<IExtension> |
| 101 | +ExtensionFactory::loadExtension(std::filesystem::path directory, |
| 102 | + ExtensionMetaData metadata) |
| 103 | +{ |
| 104 | + if (!metadata.isValid()) { |
| 105 | + log::warn("failed to load extension from '{}': invalid metadata", |
| 106 | + directory.native()); |
| 107 | + return nullptr; |
| 108 | + } |
| 109 | + |
| 110 | + switch (metadata.type()) { |
| 111 | + case ExtensionType::THEME: |
| 112 | + return ThemeExtension::loadExtension(std::move(directory), std::move(metadata)); |
| 113 | + case ExtensionType::TRANSLATION: |
| 114 | + return TranslationExtension::loadExtension(std::move(directory), |
| 115 | + std::move(metadata)); |
| 116 | + case ExtensionType::PLUGIN: |
| 117 | + case ExtensionType::GAME: |
| 118 | + case ExtensionType::INVALID: |
| 119 | + default: |
| 120 | + log::warn("failed to load extension from '{}': invalid type", directory.native()); |
| 121 | + return nullptr; |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +ThemeExtension::ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, |
| 126 | + std::vector<std::shared_ptr<const Theme>> themes) |
| 127 | + : IExtension{std::move(path), std::move(metadata)}, m_Themes{std::move(themes)} |
| 128 | +{} |
| 129 | + |
| 130 | +std::unique_ptr<ThemeExtension> |
| 131 | +ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) |
14 | 132 | {
|
15 |
| - if (!m_Loaded) { |
16 |
| - m_Plugins = fetchPlugins(); |
17 |
| - m_Loaded = true; |
| 133 | + std::vector<std::shared_ptr<const Theme>> themes; |
| 134 | + const auto& jsonThemes = metadata.json()["themes"].toObject(); |
| 135 | + for (auto it = jsonThemes.begin(); it != jsonThemes.end(); ++it) { |
| 136 | + const auto theme = parseTheme(path, it.key(), it.value().toObject()); |
| 137 | + if (theme) { |
| 138 | + themes.push_back(theme); |
| 139 | + } else { |
| 140 | + log::warn("failed to parse theme '{}' from '{}'", it.key(), path.native()); |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + if (themes.empty()) { |
| 145 | + log::error("failed to parse themes from '{}'", path.native()); |
| 146 | + return nullptr; |
18 | 147 | }
|
19 |
| - return m_Plugins; |
| 148 | + |
| 149 | + return std::unique_ptr<ThemeExtension>{ |
| 150 | + new ThemeExtension(path, metadata, std::move(themes))}; |
20 | 151 | }
|
21 | 152 |
|
22 |
| -std::vector<QObject*> IExtension::loadPlugins() |
| 153 | +std::shared_ptr<const Theme> |
| 154 | +ThemeExtension::parseTheme(std::filesystem::path const& extensionFolder, |
| 155 | + const QString& identifier, const QJsonObject& jsonTheme) |
23 | 156 | {
|
24 |
| - // loadPlugins() is just exposed to pre-load plugins |
25 |
| - return plugins(); |
| 157 | + const auto name = jsonTheme["name"].toString(); |
| 158 | + const auto filepath = |
| 159 | + extensionFolder / jsonTheme["path"].toString().toUtf8().toStdString(); |
| 160 | + |
| 161 | + if (name.isEmpty() || !is_regular_file(filepath)) { |
| 162 | + return nullptr; |
| 163 | + } |
| 164 | + |
| 165 | + return std::make_shared<Theme>(identifier.toStdString(), name.toStdString(), |
| 166 | + filepath); |
| 167 | +} |
| 168 | + |
| 169 | +TranslationExtension::TranslationExtension( |
| 170 | + std::filesystem::path path, ExtensionMetaData metadata, |
| 171 | + std::vector<std::shared_ptr<const Translation>> translations) |
| 172 | + : IExtension{std::move(path), std::move(metadata)}, |
| 173 | + m_Translations(std::move(translations)) |
| 174 | +{} |
| 175 | + |
| 176 | +std::unique_ptr<TranslationExtension> |
| 177 | +TranslationExtension::loadExtension(std::filesystem::path path, |
| 178 | + ExtensionMetaData metadata) |
| 179 | +{ |
| 180 | + std::vector<std::shared_ptr<const Translation>> translations; |
| 181 | + const auto& jsonTranslations = metadata.json()["translations"].toObject(); |
| 182 | + for (auto it = jsonTranslations.begin(); it != jsonTranslations.end(); ++it) { |
| 183 | + const auto theme = parseTranslation(path, it.key(), it.value().toObject()); |
| 184 | + if (theme) { |
| 185 | + translations.push_back(theme); |
| 186 | + } else { |
| 187 | + log::warn("failed to parse translation '{}' from '{}'", it.key(), path.native()); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + if (translations.empty()) { |
| 192 | + log::error("failed to parse translations from '{}'", path.native()); |
| 193 | + return nullptr; |
| 194 | + } |
| 195 | + |
| 196 | + return std::unique_ptr<TranslationExtension>{ |
| 197 | + new TranslationExtension(path, metadata, std::move(translations))}; |
| 198 | +} |
| 199 | + |
| 200 | +#pragma optimize("", off) |
| 201 | +std::shared_ptr<const Translation> |
| 202 | +TranslationExtension::parseTranslation(std::filesystem::path const& extensionFolder, |
| 203 | + const QString& identifier, |
| 204 | + const QJsonObject& jsonTranslation) |
| 205 | +{ |
| 206 | + const auto name = jsonTranslation["name"].toString(); |
| 207 | + const auto jsonGlobFiles = jsonTranslation["files"].toVariant().toStringList(); |
| 208 | + |
| 209 | + std::vector<std::filesystem::path> qm_files; |
| 210 | + for (const auto& globFile : jsonGlobFiles) { |
| 211 | + // use a QFileInfo to extract the name (glob) and the directory - we currently do |
| 212 | + // not handle recursive glob (**) |
| 213 | + QFileInfo globFileInfo(extensionFolder, globFile); |
| 214 | + |
| 215 | + QDirIterator dirIterator(globFileInfo.absolutePath(), {globFileInfo.fileName()}, |
| 216 | + QDir::Files); |
| 217 | + while (dirIterator.hasNext()) { |
| 218 | + dirIterator.next(); |
| 219 | + qm_files.push_back(dirIterator.fileInfo().filesystemAbsoluteFilePath()); |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + if (name.isEmpty() || qm_files.empty()) { |
| 224 | + return nullptr; |
| 225 | + } |
| 226 | + |
| 227 | + return std::make_shared<Translation>(identifier.toStdString(), name.toStdString(), |
| 228 | + std::move(qm_files)); |
26 | 229 | }
|
| 230 | +#pragma optimize("", on) |
27 | 231 |
|
28 | 232 | } // namespace MOBase
|
0 commit comments