diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 19:51:40 -0500 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-05-16 19:51:40 -0500 |
| commit | 549b84b0f8815737c93913b200dde3821a062cb0 (patch) | |
| tree | f6bfd96729016847438beda9416c93f447838077 | |
| parent | a0747cabb420d699be3a1dd4c63f47c5153bd7b9 (diff) | |
mo2: catch up to upstream 2.5.3 Betas 3-11
Multi-area sync against the upstream MO2 2.5.3 beta line. Roughly three
logical sections share the diff:
Catch-up (Betas 3-11):
- nxmaccessmanager: drop "supporter" from validRoles so non-premium
supporters get the right download path (Beta 10)
- mainwindow: disable tutorial triggers at the trigger sites; Qt 6.11
lockup on child windows (Beta 4)
- basic_games: add Kingdom Come Deliverance 2, Slay the Spire 2
(Betas 11, 6)
- uibase: add NXM collections parsing fields/accessors + (?:nxm|modl)
scheme support in NXMUrl (Beta 9)
- Starfield: blueprintPrefix() on IPluginGame; plugins.txt write
suppression for blueprint plugins; hasInvalidBlueprint /
hasUnpairedBlueprint diagnoses; Title-keyed content catalog
consolidation (Betas 3-5)
- downloadmanager + systemtraymanager + organizercore + iuserinterface
+ settings: add Beta 11 "show notifications when downloads complete
or fail" toggle and the showNotification plumbing
- nxmaccessmanager: restore upstream re-entry guard for OAuth refresh
and the styled OAuth response page (Beta 11)
- organizer_en.ts and game-bethesda *_en.ts: pull upstream HEAD strings
Workarounds tab removed:
- Delete settingsdialogworkarounds.{h,cpp}; drop the tab in
settingsdialog.ui; move "Enable archives parsing" into General
- Hardcode GameSettings::forceEnableCoreFiles() to true so the
primary-master toggle-off bug (DLCs unchecked on tab refresh) can't
recur; setter becomes a no-op
- No-op stubs for offlineMode, useProxy, useCustomBrowser, Steam
appID/credentials, executablesBlacklist, skipFileSuffixes,
skipDirectories — UI gone, accessors keep call sites green
- settingsdialognexus drops the custom-browser wiring; spawn drops the
"Change the blacklist" Retry path
Beta 9 download manager port:
- fileID-first match in nxmFilesAvailable and
nxmFileInfoFromMd5Available; filename remains the fallback
- NexusInterface::isActiveFileStatus() helper; rewrite of
nxmUpdatesAvailable to use pickNewestVersion /
findUpdateChainSuccessors / resolveInstalledFileId. ARCHIVED_HIDDEN
now correctly treated as inactive
- std::optional<unsigned int> reservedID on DownloadInfo factories and
on the three addDownload overloads; startDownloadURLs and
startDownloadURLWithMeta return the canonical DownloadID instead of
m_ActiveDownloads.size()-1 (which was the index, not an ID)
- QHash<unsigned int, DownloadInfo*> m_ByID maintained alongside
m_ActiveDownloads at every mutation site; downloadInfoByID is now
O(1) via m_ByID.value(id, nullptr)
LOOT remains intentionally stripped (lootcli build cruft cleaned up
separately). BSA wide-path skipped — Linux UTF-8 paths already work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
33 files changed, 1560 insertions, 1712 deletions
diff --git a/libs/basic_games/games/game_kingdomcomedeliverance2.py b/libs/basic_games/games/game_kingdomcomedeliverance2.py new file mode 100644 index 0000000..cef7e23 --- /dev/null +++ b/libs/basic_games/games/game_kingdomcomedeliverance2.py @@ -0,0 +1,104 @@ +import os +import xml.etree.ElementTree as ET +from pathlib import Path + +from PyQt6.QtCore import QDir, qInfo, qWarning + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_game import BasicGame + + +class KingdomComeDeliverance2Game(BasicGame): + Name = "Kingdom Come: Deliverance 2 Support Plugin" + Author = "TheForgotten69" + Version = "1.0.0" + + GameName = "Kingdom Come: Deliverance II" + GameShortName = "kingdomcomedeliverance2" + GameNexusName = "kingdomcomedeliverance2" + GameNexusId = 7286 + GameSteamId = [1771300] + GameBinary = "bin/Win64MasterMasterSteamPGO/KingdomCome.exe" + GameDataPath = "Mods" + GameSaveExtension = "whs" + GameDocumentsDirectory = "%GAME_PATH%" + GameSavesDirectory = "%USERPROFILE%/Saved Games/kingdomcome2/saves" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(BasicModDataChecker(GlobPatterns(valid=["*"]))) + organizer.onAboutToRun(self._write_mod_order) + return True + + @staticmethod + def _get_mod_id(mod_path: Path) -> str | None: + """Return the mod ID for a KCD2 mod. + + Checks mod.manifest for <modid> (preferred) or <name>, falling back + to the game mod folder name (the subfolder inside the MO2 mod directory). + """ + for manifest in mod_path.rglob("mod.manifest"): + try: + root = ET.parse(manifest).getroot() + for tag in ("modid", "name"): + elem = root.find(f".//{tag}") + if elem is not None and elem.text and elem.text.strip(): + return elem.text.strip() + except ET.ParseError: + qWarning(f"KCD2: failed to parse {manifest}") + # Fall back to the folder that contains mod.manifest + return manifest.parent.name + # No manifest — use first subdirectory name (the game mod folder) + for subdir in mod_path.iterdir(): + if subdir.is_dir(): + return subdir.name + return None + + def _write_mod_order(self, app_path: str, wd: QDir, args: str) -> bool: + if not self.isActive(): + return True + + modlist = self._organizer.modList() + mods_path = Path(self._organizer.modsPath()) + mod_order_path = Path(self._organizer.overwritePath()) / "mod_order.txt" + + mod_ids: list[str] = [] + for mod_name in modlist.allModsByProfilePriority(): + if not (modlist.state(mod_name) & mobase.ModState.ACTIVE): + continue + mod_id = self._get_mod_id(mods_path / mod_name) + if mod_id: + qInfo(f"KCD2: mod '{mod_name}' -> id '{mod_id}'") + mod_ids.append(mod_id) + else: + qWarning(f"KCD2: could not resolve id for mod '{mod_name}', skipping") + + if not mod_ids: + mod_order_path.unlink(missing_ok=True) + return True + + # MO2 priority 1 = top = highest priority. KCD2 is last-loaded-wins, + # so highest priority must be last in the file. + mod_ids.reverse() + mod_order_path.parent.mkdir(parents=True, exist_ok=True) + mod_order_path.write_text("\n".join(mod_ids)) + qInfo(f"KCD2: wrote mod_order.txt with {len(mod_ids)} mods: {mod_ids}") + return True + + def iniFiles(self): + return ["custom.cfg", "system.cfg", "user.cfg"] + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + for iniFile in self.iniFiles(): + iniPath = self.documentsDirectory().absoluteFilePath(iniFile) + if not os.path.exists(iniPath): + with open(iniPath, "w") as _: + pass + + modsPath = self.dataDirectory().absolutePath() + if not os.path.exists(modsPath): + os.mkdir(modsPath) + + super().initializeProfile(directory, settings) diff --git a/libs/basic_games/games/game_sts2.py b/libs/basic_games/games/game_sts2.py new file mode 100644 index 0000000..fe992e8 --- /dev/null +++ b/libs/basic_games/games/game_sts2.py @@ -0,0 +1,81 @@ +from pathlib import Path + +from PyQt6.QtCore import QDir, QFileInfo + +import mobase + +from ..basic_features import BasicModDataChecker, GlobPatterns +from ..basic_features.basic_save_game_info import BasicGameSaveGame +from ..basic_game import BasicGame + + +class SlayTheSpire2ModDataChecker(BasicModDataChecker): + def __init__(self): + super().__init__( + GlobPatterns( + valid=[ + "*.pck", + "*.dll", + "*.json", + ], + move={ + "*/*.pck": "", + "*/*.dll": "", + "*/*.json": "", + }, + ) + ) + + +class SlayTheSpire2Game(BasicGame): + Name = "Slay the Spire 2 Support Plugin" + Author = "Azlle" + Version = "1.0.1" + + GameName = "Slay the Spire 2" + GameShortName = "slaythespire2" + GameNexusName = "slaythespire2" + GameNexusId = 8916 + GameSteamId = 2868840 + GameBinary = "SlayTheSpire2.exe" + GameDataPath = "mods" + GameDocumentsDirectory = "%USERPROFILE%/AppData/Roaming/SlayTheSpire2" + + def init(self, organizer: mobase.IOrganizer) -> bool: + super().init(organizer) + self._register_feature(SlayTheSpire2ModDataChecker()) + return True + + def initializeProfile(self, directory: QDir, settings: mobase.ProfileSetting): + mods_path = Path(self.dataDirectory().absolutePath()) + mods_path.mkdir(exist_ok=True) + super().initializeProfile(directory, settings) + + def savesDirectory(self) -> QDir: + docs = QDir(self.documentsDirectory()) + steam_dir = Path(docs.absoluteFilePath("steam")) + if steam_dir.exists(): + entries = [e for e in steam_dir.iterdir() if e.is_dir()] + if entries: + return QDir(str(entries[0])) + return QDir(str(steam_dir)) + + def listSaves(self, folder: QDir) -> list[mobase.ISaveGame]: + base = Path(folder.absolutePath()) + return [ + BasicGameSaveGame(save) + for save in base.rglob("*") + if save.is_file() and save.suffix in (".save", ".run") + ] + + def executables(self): + return [ + mobase.ExecutableInfo( + "Slay the Spire 2", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ), + mobase.ExecutableInfo( + "Slay the Spire 2 (OpenGL)", + QFileInfo(self.gameDirectory().absoluteFilePath(self.binaryName())), + ).withArgument("--rendering-driver opengl3"), + ] diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp index ebec948..06a6d79 100644 --- a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp @@ -22,6 +22,7 @@ #include <QDir> #include <QFileInfo> #include <QList> +#include <QRegularExpression> #include <QObject> #include <QString> #include <QStringList> @@ -337,15 +338,23 @@ QStringList GameStarfield::CCPlugins() const // as Creations. The StarfieldUnmanagedMods class handles parsing mod names and files. if (unmanagedMods.get()) { auto contentCatalog = unmanagedMods->parseContentCatalog(); - for (const auto& mod : contentCatalog) { - if (!plugins.contains(mod.first, Qt::CaseInsensitive)) { - plugins.append(mod.first); + for (auto& mod : contentCatalog) { + QStringList pluginFiles = mod.second.files.filter(QRegularExpression( + "\\.es(m|p|l)$", QRegularExpression::CaseInsensitiveOption)); + if (!pluginFiles.isEmpty()) { + plugins += pluginFiles; } } } + plugins.removeDuplicates(); return plugins; } +QString GameStarfield::blueprintPrefix() const +{ + return "blueprintships-"; +} + IPluginGame::SortMechanism GameStarfield::sortMechanism() const { return IPluginGame::SortMechanism::LOOT; @@ -380,6 +389,10 @@ std::vector<unsigned int> GameStarfield::activeProblems() const if (testFilePresent()) result.push_back(PROBLEM_TEST_FILE); } + if (hasInvalidBlueprint()) + result.push_back(PROBLEM_INVALID_BLUEPRINT); + if (hasUnpairedBlueprint()) + result.push_back(PROBLEM_UNPAIRED_BLUEPRINT); } return result; } @@ -412,6 +425,38 @@ bool GameStarfield::testFilePresent() const return false; } +bool GameStarfield::hasInvalidBlueprint() const +{ + auto plugins = m_Organizer->pluginList()->pluginNames(); + for (auto plugin : plugins) { + if (m_Organizer->pluginList()->isBlueprintFlagged(plugin)) { + if (!plugin.startsWith(blueprintPrefix(), Qt::CaseInsensitive) || + !m_Organizer->pluginList()->hasMasterExtension(plugin)) + return true; + } else if (plugin.startsWith(blueprintPrefix(), Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +bool GameStarfield::hasUnpairedBlueprint() const +{ + auto plugins = m_Organizer->pluginList()->pluginNames(); + for (auto plugin : plugins) { + if (plugin.startsWith(blueprintPrefix(), Qt::CaseInsensitive) && + m_Organizer->pluginList()->hasMasterExtension(plugin)) { + QString parent = plugin.mid(blueprintPrefix().size(), + plugin.size() - blueprintPrefix().size() - 4); + auto mainPlugin = plugins.filter(QRegularExpression( + "^" + parent + "\\.es(m|p|l)$", QRegularExpression::CaseInsensitiveOption)); + if (mainPlugin.isEmpty()) + return true; + } + } + return false; +} + QString GameStarfield::shortDescription(unsigned int key) const { switch (key) { @@ -419,6 +464,10 @@ QString GameStarfield::shortDescription(unsigned int key) const return tr("You have active ESP plugins in Starfield"); case PROBLEM_TEST_FILE: return tr("sTestFile entries are present"); + case PROBLEM_INVALID_BLUEPRINT: + return tr("Invalid blueprint plugins found"); + case PROBLEM_UNPAIRED_BLUEPRINT: + return tr("Unpaired blueprint plugins found"); } return ""; } @@ -446,6 +495,17 @@ QString GameStarfield::fullDescription(unsigned int key) const "settings in your StarfieldCustom.ini. These must be removed or the game " "will not read the plugins.txt file. Management is still disabled.</p>"); } + case PROBLEM_INVALID_BLUEPRINT: + return tr( + "<p>You have a blueprint file that is invalid. Blueprints require the " + "blueprint flag and prefix and must have the ESM extension to be valid.</p>"); + case PROBLEM_UNPAIRED_BLUEPRINT: + return tr( + "<p>You have a valid blueprint file that has no paired main plugin. The only " + "way to load blueprint files is by enabling a main plugin with the same base " + "name. This is the part after the prefix and before the extension.</p><p>eg. " + "<strong>BlueprintShips-example.esm</strong> should have a paired main plugin " + "<strong>example.esm</strong></p>"); } return ""; } diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.h b/libs/game_bethesda/src/games/starfield/gamestarfield.h index f9724c0..a512b36 100644 --- a/libs/game_bethesda/src/games/starfield/gamestarfield.h +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.h @@ -41,6 +41,7 @@ public: // IPluginGame interface virtual bool prepareIni(const QString& exec) override; virtual QStringList DLCPlugins() const override; virtual QStringList CCPlugins() const override; + virtual QString blueprintPrefix() const override; virtual SortMechanism sortMechanism() const override; virtual LoadOrderMechanism loadOrderMechanism() const override; virtual int nexusModOrganizerID() const override; @@ -72,10 +73,14 @@ private: QStringList CCCPlugins() const; bool activeESP() const; bool testFilePresent() const; + bool hasInvalidBlueprint() const; + bool hasUnpairedBlueprint() const; private: - static const unsigned int PROBLEM_ESP = 1; - static const unsigned int PROBLEM_TEST_FILE = 2; + static const unsigned int PROBLEM_ESP = 1; + static const unsigned int PROBLEM_TEST_FILE = 2; + static const unsigned int PROBLEM_INVALID_BLUEPRINT = 3; + static const unsigned int PROBLEM_UNPAIRED_BLUEPRINT = 4; mutable std::set<QString> m_Active_ESPs; }; diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp index 6b4e6ab..efd1bad 100644 --- a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp @@ -1,5 +1,9 @@ #include "starfieldgameplugins.h" +#include <ipluginlist.h> +#include <report.h> +#include <safewritefile.h> + using namespace MOBase; StarfieldGamePlugins::StarfieldGamePlugins(MOBase::IOrganizer* organizer) @@ -19,10 +23,76 @@ bool StarfieldGamePlugins::blueprintPluginsAreSupported() void StarfieldGamePlugins::writePluginList(const IPluginList* pluginList, const QString& filePath) { - if (m_Organizer->managedGame()->loadOrderMechanism() == + if (m_Organizer->managedGame()->loadOrderMechanism() != MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { - CreationGamePlugins::writePluginList(pluginList, filePath); + return; + } + + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write(encoder.encode( + "# This file was automatically generated by Mod Organizer.\r\n")); + + bool invalidFileNames = false; + int writtenCount = 0; + + QStringList plugins = pluginList->pluginNames(); + std::sort(plugins.begin(), plugins.end(), + [pluginList](const QString& lhs, const QString& rhs) { + return pluginList->priority(lhs) < pluginList->priority(rhs); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet<QString> ManagedMods = + QSet<QString>(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet<QString> DLCSet = QSet<QString>(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList<QString>(ManagedMods.begin(), ManagedMods.end())); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive) && + !pluginList->isBlueprintFlagged(pluginName) && + !pluginName.startsWith(m_Organizer->managedGame()->blueprintPrefix(), + Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } + } } + + if (invalidFileNames) { + reportError(QObject::tr("Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.")); + } + + file->commit(); } QStringList StarfieldGamePlugins::readPluginList(MOBase::IPluginList* pluginList) diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.h b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h index 7bd07e0..7b1427d 100644 --- a/libs/game_bethesda/src/games/starfield/starfieldsavegame.h +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.h @@ -12,6 +12,11 @@ class StarfieldSaveGame : public GamebryoSaveGame public: StarfieldSaveGame(QString const& fileName, GameStarfield const* game); + const QStringList v122CorePlugins = {"Starfield.esm", "Constellation.esm", + "OldMars.esm", "BlueprintShips-Starfield.esm", + "SFBGS003.esm", "SFBGS006.esm", + "SFBGS007.esm", "SFBGS008.esm"}; + protected: // Fetch easy-to-access information. void getData(FileWrapper& file) const; diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp index 30abb38..82207c3 100644 --- a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp @@ -9,7 +9,7 @@ StarfieldUnmanagedMods::StarfieldUnmanagedMods(const GameStarfield* game, const QString& appDataFolder) - : GamebryoUnmangedMods(game), m_AppDataFolder(appDataFolder) + : GamebryoUnmangedMods(game), m_Game(game), m_AppDataFolder(appDataFolder) {} StarfieldUnmanagedMods::~StarfieldUnmanagedMods() {} @@ -25,29 +25,52 @@ QStringList StarfieldUnmanagedMods::mods(bool onlyOfficial) const pluginList.removeAll(plugin); } QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; + auto contentCatalog = parseContentCatalog(); directories.insert(game()->secondaryDataDirectories()); for (QDir directory : directories) { for (const QString& fileName : directory.entryList({"*.esp", "*.esl", "*.esm"})) { if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { - result.append(fileName.chopped(4)); // trims the extension off + bool found = false; + for (const auto& mod : contentCatalog) { + if (mod.second.files.contains(fileName, Qt::CaseInsensitive)) { + result.append(mod.second.name); + found = true; + } + } + if (!found && !fileName.startsWith("blueprintships-", Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } } } } } - + result.removeDuplicates(); return result; } QFileInfo StarfieldUnmanagedMods::referenceFile(const QString& modName) const { - QFileInfoList files; + QStringList modFiles; + auto contentCatalog = parseContentCatalog(); + if (contentCatalog.contains(modName)) { + modFiles = contentCatalog[modName].files; + } + QDir dataDir = m_Game->secondaryDataDirectories().first(); + QStringList plugins = modFiles.filter( + QRegularExpression("\\.es(m|p|l)$", QRegularExpression::CaseInsensitiveOption)); QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; directories.insert(game()->secondaryDataDirectories()); + QFileInfoList pluginFiles; + QFileInfoList files; for (QDir directory : directories) { + if (!plugins.isEmpty()) + pluginFiles += dataDir.entryInfoList(plugins); files += directory.entryInfoList(QStringList() << modName + ".es*"); } - if (files.size() > 0) { + if (!pluginFiles.isEmpty()) { + return pluginFiles.at(0); + } else if (!files.isEmpty()) { return files.at(0); } else { return QFileInfo(); @@ -86,11 +109,10 @@ StarfieldUnmanagedMods::parseContentCatalog() const pluginList.append(fileName); } } - for (const auto& plugin : pluginList) { - contentCatalog[plugin] = ContentCatalog(); - contentCatalog[plugin].files = files; - contentCatalog[plugin].name = modInfo.value("Title").toString(); - } + QString name = modInfo.value("Title").toString(); + contentCatalog[name] = ContentCatalog(); + contentCatalog[name].files = files; + contentCatalog[name].name = name; } } } @@ -101,17 +123,16 @@ QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const { QStringList files; auto contentCatalog = parseContentCatalog(); - for (const auto& mod : contentCatalog) { - if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { - files += mod.second.files; - break; - } + if (contentCatalog.contains(modName)) { + return contentCatalog[modName].files; } // file extension in FO4 is .ba2 instead of bsa QMap<QString, QDir> directories = {{"data", game()->dataDirectory()}}; directories.insert(game()->secondaryDataDirectories()); for (QDir directory : directories) { - for (const QString& archiveName : directory.entryList({modName + "*.ba2"})) { + for (const QString& archiveName : + directory.entryList({modName + "*.ba2", "blueprintships-" + modName + ".esm", + "blueprintships-" + modName + "*.ba2"})) { files.append(directory.absoluteFilePath(archiveName)); } } @@ -121,12 +142,12 @@ QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const QString StarfieldUnmanagedMods::displayName(const QString& modName) const { auto contentCatalog = parseContentCatalog(); - for (const auto& mod : contentCatalog) { - if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { - return mod.second.name; - } + if (contentCatalog.contains(modName)) { + return contentCatalog[modName].name; } - // unlike in earlier games, in fallout 4 the file name doesn't correspond to - // the public name + if (modName == "ShatteredSpace") + return "Shattered Space"; + else if (modName == "SFBGS050") + return "Terran Armada"; return modName; } diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h index abe14ef..81fd63d 100644 --- a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.h @@ -30,6 +30,7 @@ private: std::map<QString, ContentCatalog> parseContentCatalog() const; private: + const GameStarfield* m_Game; QString m_AppDataFolder; }; diff --git a/libs/uibase/include/uibase/iplugingame.h b/libs/uibase/include/uibase/iplugingame.h index 93ee09f..c7760f1 100644 --- a/libs/uibase/include/uibase/iplugingame.h +++ b/libs/uibase/include/uibase/iplugingame.h @@ -314,6 +314,13 @@ public: */ virtual QStringList CCPlugins() const { return {}; } + /** + * @brief Get the blueprint plugin prefix for blueprint autoloading + * + * @node Primarily used by Starfield + */ + virtual QString blueprintPrefix() const { return ""; } + /* * @brief determine the load order mechanism used by this game. * diff --git a/libs/uibase/include/uibase/nxmurl.h b/libs/uibase/include/uibase/nxmurl.h index bd06d3d..dd1935e 100644 --- a/libs/uibase/include/uibase/nxmurl.h +++ b/libs/uibase/include/uibase/nxmurl.h @@ -75,6 +75,21 @@ public: */ int userId() const { return m_UserId; } + /** + * @return if collection link + */ + bool isCollection() const { return m_Collection; } + + /** + * @return collection ID + */ + QString collectionId() const { return m_CollectionId; } + + /** + * @return collection revision + */ + int collectionRevision() const { return m_CollectionRevision; } + private: QString m_Game; QString m_Key; @@ -82,6 +97,9 @@ private: int m_FileId; int m_Expires; int m_UserId; + bool m_Collection; + QString m_CollectionId; + int m_CollectionRevision; }; #endif // NXMURL_H diff --git a/libs/uibase/src/nxmurl.cpp b/libs/uibase/src/nxmurl.cpp index 33bb22f..bdb9f7b 100644 --- a/libs/uibase/src/nxmurl.cpp +++ b/libs/uibase/src/nxmurl.cpp @@ -29,16 +29,29 @@ NXMUrl::NXMUrl(const QString& url) { QUrl nxm(url); QUrlQuery query(nxm); - QRegularExpression exp("(?:nxm|modl)://[a-z0-9]+/mods/(\\d+)/files/(\\d+)", - QRegularExpression::CaseInsensitiveOption); - auto match = exp.match(url); - if (!match.hasMatch()) { + + static const QRegularExpression modRegex( + "(?:nxm|modl)://[a-z0-9]+/mods/(\\d+)/files/(\\d+)", + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression collectionRegex( + "(?:nxm|modl)://[a-z0-9]+/collections/([a-z0-9]+)/revisions/(\\d+)", + QRegularExpression::CaseInsensitiveOption); + + m_Game = nxm.host(); + + if (const auto modMatch = modRegex.match(url); modMatch.hasMatch()) { + m_Collection = false; + m_ModId = modMatch.captured(1).toInt(); + m_FileId = modMatch.captured(2).toInt(); + m_Key = query.queryItemValue("key"); + m_Expires = query.queryItemValue("expires").toInt(); + m_UserId = query.queryItemValue("user_id").toInt(); + } else if (const auto collectionMatch = collectionRegex.match(url); + collectionMatch.hasMatch()) { + m_Collection = true; + m_CollectionId = collectionMatch.captured(1); + m_CollectionRevision = collectionMatch.captured(2).toInt(); + } else { throw MOBase::InvalidNXMLinkException(url); } - m_Game = nxm.host(); - m_ModId = match.captured(1).toInt(); - m_FileId = match.captured(2).toInt(); - m_Key = query.queryItemValue("key"); - m_Expires = query.queryItemValue("expires").toInt(); - m_UserId = query.queryItemValue("user_id").toInt(); } diff --git a/src/src/downloadmanager.cpp b/src/src/downloadmanager.cpp index d7e5ce5..46e99aa 100644 --- a/src/src/downloadmanager.cpp +++ b/src/src/downloadmanager.cpp @@ -85,10 +85,11 @@ int DownloadManager::m_DirWatcherDisabler = 0; DownloadManager::DownloadInfo* DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo* fileInfo, - const QStringList& URLs) + const QStringList& URLs, + std::optional<unsigned int> reservedID) { DownloadInfo* info = new DownloadInfo; - info->m_DownloadID = s_NextDownloadID++; + info->m_DownloadID = reservedID.value_or(s_NextDownloadID++); info->m_StartTime.start(); info->m_PreResumeSize = 0LL; info->m_Progress = std::make_pair<int, QString>(0, "0.0 B/s "); @@ -107,7 +108,8 @@ DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo* fileInfo, DownloadManager::DownloadInfo* DownloadManager::DownloadInfo::createFromMeta(const QString& filePath, bool showHidden, const QString outputDirectory, - std::optional<uint64_t> fileSize) + std::optional<uint64_t> fileSize, + std::optional<unsigned int> reservedID) { DownloadInfo* info = new DownloadInfo; @@ -144,7 +146,7 @@ DownloadManager::DownloadInfo::createFromMeta(const QString& filePath, bool show } } - info->m_DownloadID = s_NextDownloadID++; + info->m_DownloadID = reservedID.value_or(s_NextDownloadID++); info->m_Output.setFileName(filePath); info->m_TotalSize = fileSize ? *fileSize : QFileInfo(filePath).size(); info->m_PreResumeSize = info->m_TotalSize; @@ -269,6 +271,7 @@ DownloadManager::~DownloadManager() delete *iter; } m_ActiveDownloads.clear(); + m_ByID.clear(); } void DownloadManager::setParentWidget(QWidget* w) @@ -370,6 +373,7 @@ void DownloadManager::refreshList() iter != m_ActiveDownloads.end();) { if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { + m_ByID.remove((*iter)->m_DownloadID); delete *iter; iter = m_ActiveDownloads.erase(iter); } else { @@ -454,6 +458,7 @@ void DownloadManager::refreshList() } cx.self.m_ActiveDownloads.push_front(info); + cx.self.m_ByID.insert(info->m_DownloadID, info); cx.seen.insert(std::move(lc)); cx.seen.insert( QFileInfo(info->m_Output.fileName()).fileName().toLower().toStdWString()); @@ -499,7 +504,8 @@ void DownloadManager::queryDownloadListInfo() } bool DownloadManager::addDownload(const QStringList& URLs, QString gameName, int modID, - int fileID, const ModRepositoryFileInfo* fileInfo) + int fileID, const ModRepositoryFileInfo* fileInfo, + std::optional<unsigned int> reservedID) { // Parse the URL properly instead of feeding it to QFileInfo — QFileInfo // doesn't understand URLs, so its fileName() can pull in query-string junk @@ -561,7 +567,7 @@ bool DownloadManager::addDownload(const QStringList& URLs, QString gameName, int QNetworkRequest::AlwaysNetwork); request.setHttp2Configuration(h2Conf); return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, - gameName, modID, fileID, fileInfo); + gameName, modID, fileID, fileInfo, reservedID); } bool DownloadManager::addDownload(QNetworkReply* reply, @@ -578,11 +584,12 @@ bool DownloadManager::addDownload(QNetworkReply* reply, bool DownloadManager::addDownload(QNetworkReply* reply, const QStringList& URLs, const QString& fileName, QString gameName, int modID, - int fileID, const ModRepositoryFileInfo* fileInfo) + int fileID, const ModRepositoryFileInfo* fileInfo, + std::optional<unsigned int> reservedID) { // download invoked from an already open network reply (i.e. download link in the // browser) - DownloadInfo* newDownload = DownloadInfo::createNew(fileInfo, URLs); + DownloadInfo* newDownload = DownloadInfo::createNew(fileInfo, URLs, reservedID); QString baseName = fileName; if (!fileInfo->fileName.isEmpty()) { @@ -675,6 +682,7 @@ void DownloadManager::startDownload(QNetworkReply* reply, DownloadInfo* newDownl emit aboutToUpdate(); m_ActiveDownloads.append(newDownload); + m_ByID.insert(newDownload->m_DownloadID, newDownload); emit update(-1); emit downloadAdded(); @@ -986,6 +994,7 @@ void DownloadManager::removeDownload(int index, bool deleteFile) if ((removeAll && (downloadState >= STATE_READY)) || (removeState == downloadState)) { removeFile(index, deleteFile); + m_ByID.remove((*iter)->m_DownloadID); delete *iter; iter = m_ActiveDownloads.erase(iter); } else { @@ -1001,6 +1010,7 @@ void DownloadManager::removeDownload(int index, bool deleteFile) } removeFile(index, deleteFile); + m_ByID.remove(m_ActiveDownloads.at(index)->m_DownloadID); delete m_ActiveDownloads.at(index); m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); } @@ -1115,15 +1125,11 @@ void DownloadManager::resumeDownloadInt(int index) DownloadManager::DownloadInfo* DownloadManager::downloadInfoByID(unsigned int id) { - auto iter = std::find_if(m_ActiveDownloads.begin(), m_ActiveDownloads.end(), - [id](DownloadInfo* info) { - return info->m_DownloadID == id; - }); - if (iter != m_ActiveDownloads.end()) { - return *iter; - } else { - return nullptr; - } + // O(1) via m_ByID; the prior linear scan over m_ActiveDownloads couldn't + // distinguish "never existed" from "valid but waiting on a callback that + // already erased it," so late callbacks would dereference freed memory. + // Callers treat nullptr as the canonical "stale ID" signal. + return m_ByID.value(id, nullptr); } void DownloadManager::queryInfo(int index) @@ -1425,8 +1431,11 @@ QString DownloadManager::getDisplayName(int index) const throw MyException(tr("display name: invalid download index %1").arg(index)); } - DownloadInfo* info = m_ActiveDownloads.at(index); + return getDisplayNameForInfo(m_ActiveDownloads.at(index)); +} +QString DownloadManager::getDisplayNameForInfo(const DownloadInfo* info) const +{ QTextDocument doc; if (!info->m_FileInfo->name.isEmpty()) { doc.setHtml(info->m_FileInfo->name); @@ -1861,6 +1870,11 @@ void DownloadManager::nxmDescriptionAvailable(QString, int, QVariant userData, info->m_FileInfo->modName = doc.toPlainText(); if (info->m_FileInfo->fileID != 0) { setState(info, STATE_READY); + if (m_OrganizerCore->settings().interface().showDownloadNotifications()) { + m_OrganizerCore->showNotification( + tr("Download complete"), + tr("%1 is ready to be installed.").arg(getDisplayNameForInfo(info))); + } } else { setState(info, STATE_FETCHINGFILEINFO); } @@ -1893,33 +1907,59 @@ void DownloadManager::nxmFilesAvailable(QString, int, QVariant userData, alternativeLocalName = match.captured(1); } + // Copy the matched Nexus file metadata onto info. Used by both passes + // below so the fileID-priority path and the filename-fallback path stay + // consistent. + auto applyMatch = [&info](const QVariantMap& fileInfo) { + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + if (!info->m_FileInfo->version.isValid()) { + info->m_FileInfo->version = info->m_FileInfo->newestVersion; + } + info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); + info->m_FileInfo->fileTime = + QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong()); + info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); + info->m_FileInfo->fileName = fileInfo["file_name"].toString(); + info->m_FileInfo->description = + BBCode::convertToHTML(fileInfo["description"].toString()); + info->m_FileInfo->author = fileInfo["author"].toString(); + info->m_FileInfo->uploader = fileInfo["uploaded_by"].toString(); + info->m_FileInfo->uploaderUrl = fileInfo["uploaded_users_profile_url"].toString(); + }; + bool found = false; - for (const QVariant& file : files) { - QVariantMap fileInfo = file.toMap(); - QString const fileName = fileInfo["file_name"].toString(); - QString const fileNameVariant = fileName.mid(0).replace(' ', '_'); - if ((fileName == info->m_RemoteFileName) || - (fileNameVariant == info->m_RemoteFileName) || (fileName == info->m_FileName) || - (fileNameVariant == info->m_FileName) || (fileName == alternativeLocalName) || - (fileNameVariant == alternativeLocalName)) { - info->m_FileInfo->name = fileInfo["name"].toString(); - info->m_FileInfo->version.parse(fileInfo["version"].toString()); - if (!info->m_FileInfo->version.isValid()) { - info->m_FileInfo->version = info->m_FileInfo->newestVersion; + // Pass 1: prefer matching by Nexus file_id when we already have one. + // file_id is canonical; filename can disagree after a local rename or + // a Nexus-side filename change. + if (info->m_FileInfo->fileID != 0) { + for (const QVariant& file : files) { + QVariantMap const fileInfo = file.toMap(); + if (fileInfo["file_id"].toInt() == info->m_FileInfo->fileID) { + applyMatch(fileInfo); + found = true; + break; + } + } + } + + // Pass 2: fall back to filename matching (also the only path when we + // don't have a usable fileID yet). + if (!found) { + for (const QVariant& file : files) { + QVariantMap fileInfo = file.toMap(); + QString const fileName = fileInfo["file_name"].toString(); + QString const fileNameVariant = fileName.mid(0).replace(' ', '_'); + if ((fileName == info->m_RemoteFileName) || + (fileNameVariant == info->m_RemoteFileName) || + (fileName == info->m_FileName) || (fileNameVariant == info->m_FileName) || + (fileName == alternativeLocalName) || + (fileNameVariant == alternativeLocalName)) { + applyMatch(fileInfo); + found = true; + break; } - info->m_FileInfo->fileCategory = fileInfo["category_id"].toInt(); - info->m_FileInfo->fileTime = - QDateTime::fromMSecsSinceEpoch(fileInfo["uploaded_timestamp"].toLongLong()); - info->m_FileInfo->fileID = fileInfo["file_id"].toInt(); - info->m_FileInfo->fileName = fileInfo["file_name"].toString(); - info->m_FileInfo->description = - BBCode::convertToHTML(fileInfo["description"].toString()); - info->m_FileInfo->author = fileInfo["author"].toString(); - info->m_FileInfo->uploader = fileInfo["uploaded_by"].toString(); - info->m_FileInfo->uploaderUrl = fileInfo["uploaded_users_profile_url"].toString(); - found = true; - break; } } @@ -2049,9 +2089,14 @@ bool ServerByPreference(const ServerList::container& preferredServers, int DownloadManager::startDownloadURLs(const QStringList& urls) { + // Reserve the download ID up front so the caller (plugin via IOrganizer) + // gets the canonical ID rather than a stale m_ActiveDownloads index. + const unsigned int reservedID = DownloadInfo::newDownloadID(); ModRepositoryFileInfo const info; - addDownload(urls, "", -1, -1, &info); - return m_ActiveDownloads.size() - 1; + if (!addDownload(urls, "", -1, -1, &info, reservedID)) { + return 0; + } + return static_cast<int>(reservedID); } int DownloadManager::startDownloadURLWithMeta(const QString& url, const QString& name, @@ -2059,15 +2104,16 @@ int DownloadManager::startDownloadURLWithMeta(const QString& url, const QString& const QString& version, const QString& source) { + const unsigned int reservedID = DownloadInfo::newDownloadID(); ModRepositoryFileInfo info; info.name = name; info.modName = modName; info.version = version; info.repository = source; - if (!addDownload(QStringList() << url, "", -1, -1, &info)) { + if (!addDownload(QStringList() << url, "", -1, -1, &info, reservedID)) { return 0; } - return m_ActiveDownloads.size() - 1; + return static_cast<int>(reservedID); } int DownloadManager::startDownloadNexusFile(const QString& gameName, int modID, @@ -2182,6 +2228,21 @@ void DownloadManager::nxmFileInfoFromMd5Available(QString gameName, QVariant use auto resultlist = resultData.toList(); int chosenIdx = resultlist.count() > 1 ? -1 : 0; + // Prefer matching by Nexus file_id when we already have one. file_id is + // canonical; the filename pass below can mis-pick after a local rename + // or a Nexus-side filename change. + if (chosenIdx < 0 && info->m_FileInfo->fileID != 0) { + for (int i = 0; i < resultlist.count(); i++) { + auto results = resultlist[i].toMap(); + auto fileDetails = results["file_details"].toMap(); + + if (fileDetails["file_id"].toInt() == info->m_FileInfo->fileID) { + chosenIdx = i; + break; + } + } + } + // Look for the exact file name if (chosenIdx < 0) { for (int i = 0; i < resultlist.count(); i++) { @@ -2315,6 +2376,7 @@ void DownloadManager::nxmRequestFailed(QString gameName, int modID, int fileID, if (info->m_FileInfo->modID == modID) { if (info->m_State < STATE_FETCHINGMODINFO) { + m_ByID.remove(info->m_DownloadID); m_ActiveDownloads.erase(iter); delete info; } else { @@ -2372,6 +2434,12 @@ void DownloadManager::downloadFinished(int index) emit showMessage(tr("Download failed: %1 (%2)") .arg(reply->errorString()) .arg(reply->error())); + if (m_OrganizerCore->settings().interface().showDownloadNotifications()) { + m_OrganizerCore->showNotification( + tr("Download failed"), + tr("%1 failed to download.").arg(getDisplayNameForInfo(info)), + QSystemTrayIcon::MessageIcon::Critical); + } } error = true; setState(info, STATE_ERROR); @@ -2390,6 +2458,7 @@ void DownloadManager::downloadFinished(int index) if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { emit aboutToUpdate(); info->m_Output.remove(); + m_ByID.remove(info->m_DownloadID); delete info; m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); if (error) diff --git a/src/src/downloadmanager.h b/src/src/downloadmanager.h index dce9069..9a4b568 100644 --- a/src/src/downloadmanager.h +++ b/src/src/downloadmanager.h @@ -117,10 +117,20 @@ private: bool m_Hidden{false}; static DownloadInfo* createNew(const MOBase::ModRepositoryFileInfo* fileInfo, - const QStringList& URLs); + const QStringList& URLs, + std::optional<unsigned int> reservedID = {}); static DownloadInfo* createFromMeta(const QString& filePath, bool showHidden, QString outputDirectory, - std::optional<uint64_t> fileSize = {}); + std::optional<uint64_t> fileSize = {}, + std::optional<unsigned int> reservedID = {}); + + /** + * @brief Allocate a fresh download ID without creating a DownloadInfo. + * Used by callers (notably addNXMDownload) that want to dedupe a + * pending NXM link against the ID it will eventually own, before + * the actual DownloadInfo is materialized. + */ + static unsigned int newDownloadID() { return s_NextDownloadID++; } /** * @brief rename the file @@ -238,7 +248,8 @@ public: bool addDownload(QNetworkReply* reply, const QStringList& URLs, const QString& fileName, QString gameName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo* fileInfo = - new MOBase::ModRepositoryFileInfo()); + new MOBase::ModRepositoryFileInfo(), + std::optional<unsigned int> reservedID = {}); /** * @brief start a download using a nxm-link @@ -290,6 +301,8 @@ public: **/ QString getDisplayName(int index) const; + QString getDisplayNameForInfo(const DownloadInfo* info) const; + /** * @brief retrieve the filename of the download specified by index * @@ -572,7 +585,8 @@ private: *only happens if there is a duplicate and the user decides not to download again **/ bool addDownload(const QStringList& URLs, QString gameName, int modID, int fileID, - const MOBase::ModRepositoryFileInfo* fileInfo); + const MOBase::ModRepositoryFileInfo* fileInfo, + std::optional<unsigned int> reservedID = {}); // important: the caller has to lock the list-mutex, otherwise the // DownloadInfo-pointer might get invalidated at any time @@ -609,6 +623,12 @@ private: QVector<DownloadInfo*> m_ActiveDownloads; + // O(1) DownloadID → DownloadInfo* lookup, maintained in parallel with + // m_ActiveDownloads. Lets late callbacks detect a stale ID via + // m_ByID.contains() instead of a linear m_ActiveDownloads scan that + // can't distinguish "not found" from "found but invalidated". + QHash<unsigned int, DownloadInfo*> m_ByID; + QString m_OutputDirectory; std::set<int> m_RequestIDs; QVector<int> m_AlphabeticalTranslation; diff --git a/src/src/iuserinterface.h b/src/src/iuserinterface.h index 9ff0df1..8ebdef5 100644 --- a/src/src/iuserinterface.h +++ b/src/src/iuserinterface.h @@ -4,6 +4,7 @@ #include "modinfodialogfwd.h"
#include <QMainWindow>
#include <QMenu>
+#include <QSystemTrayIcon>
#include <delayedfilewriter.h>
#include <ipluginmodpage.h>
#include <iplugintool.h>
@@ -27,6 +28,10 @@ public: virtual MOBase::DelayedFileWriterBase& archivesWriter() = 0;
virtual QMainWindow* mainWindow() = 0;
+
+ virtual void showNotification(const QString& title, const QString& message,
+ QSystemTrayIcon::MessageIcon icon =
+ QSystemTrayIcon::MessageIcon::Information) = 0;
};
#endif // IUSERINTERFACE_H
diff --git a/src/src/mainwindow.cpp b/src/src/mainwindow.cpp index ffdd4bb..941b7c7 100644 --- a/src/src/mainwindow.cpp +++ b/src/src/mainwindow.cpp @@ -1309,11 +1309,13 @@ void MainWindow::showEvent(QShowEvent* event) connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); // only the first time the window becomes visible + /* Disabling tutorials m_Tutorial.registerControl(); hookUpWindowTutorials(); - + */ if (m_OrganizerCore.settings().firstStart()) { + /* Disabling tutorials QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { if (shouldStartTutorial()) { @@ -1329,7 +1331,7 @@ void MainWindow::showEvent(QShowEvent* event) QObject::tr("Please use \"Help\" from the toolbar to get " "usage instructions to all elements")); } - + */ if (!m_OrganizerCore.managedGame()->getSupportURL().isEmpty()) { QMessageBox::information(this, tr("Game Support Wiki"), tr("Do you know how to mod this game? Do you need to " @@ -2367,6 +2369,14 @@ QMainWindow* MainWindow::mainWindow() return this; } +void MainWindow::showNotification(const QString& title, const QString& message, + QSystemTrayIcon::MessageIcon icon) +{ + if (m_SystemTrayManager) { + m_SystemTrayManager->showNotification(title, message, icon); + } +} + void MainWindow::on_tabWidget_currentChanged(int index) { QWidget* currentWidget = ui->tabWidget->widget(index); @@ -3385,138 +3395,184 @@ void MainWindow::finishUpdateInfo(const NxmUpdateInfoData& data) } } -void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, - QVariant resultData, int requestID) +namespace { - QVariantMap resultInfo = resultData.toMap(); - QList const files = resultInfo["files"].toList(); - QList const fileUpdates = resultInfo["file_updates"].toList(); - QString gameNameReal; - for (IPluginGame* game : m_PluginContainer.plugins<IPluginGame>()) { - if (game->gameNexusName() == gameName) { - gameNameReal = game->gameShortName(); +/** + * @brief Walk the file_updates chain starting at installedFileId and return + * the ordered list of successor file_ids (oldest first). + */ +std::vector<int> +findUpdateChainSuccessors(int installedFileId, + const QHash<int, QVariantMap>& updatesByOldId) +{ + std::vector<int> successors; + QSet<int> visited; + int currentId = installedFileId; + + while (true) { + const auto updateIt = updatesByOldId.constFind(currentId); + if (updateIt == updatesByOldId.constEnd()) { break; } - } - - std::vector<ModInfo::Ptr> const modsList = ModInfo::getByModID(gameNameReal, modID); - bool requiresInfo = false; + currentId = updateIt.value()["new_file_id"].toInt(); + if (visited.contains(currentId)) { + break; + } + visited.insert(currentId); + successors.push_back(currentId); + } - for (const auto& mod : modsList) { - QString validNewVersion; - int newModStatus = -1; - QString const installedFile = QFileInfo(mod->installationFile()).fileName(); + return successors; +} - if (!installedFile.isEmpty()) { - QVariantMap foundFileData; +/** + * @brief Resolve the Nexus file_id of a mod's installed file. We don't yet + * persist a nexusFileId on ModInfoRegular like upstream does, so fall + * back to matching by filename against both the files list and the + * update chain (archived-hidden files can disappear from the files list + * but still appear in the update chain). + */ +std::optional<int> resolveInstalledFileId(const ModInfo::Ptr& mod, + const QList<QVariant>& files, + const QList<QVariant>& fileUpdates) +{ + const QString installedFileName = QFileInfo(mod->installationFile()).fileName(); + if (installedFileName.isEmpty()) { + return std::nullopt; + } - // update the file status - for (const auto& file : files) { - QVariantMap fileData = file.toMap(); + for (const auto& file : files) { + const auto fileData = file.toMap(); + if (fileData["file_name"].toString().compare(installedFileName, + Qt::CaseInsensitive) == 0) { + return fileData["file_id"].toInt(); + } + } - if (fileData["file_name"].toString().compare(installedFile, - Qt::CaseInsensitive) == 0) { - foundFileData = fileData; - newModStatus = foundFileData["category_id"].toInt(); + for (const auto& updateEntry : fileUpdates) { + const auto updateData = updateEntry.toMap(); + if (installedFileName.compare(updateData["old_file_name"].toString(), + Qt::CaseInsensitive) == 0) { + return updateData["old_file_id"].toInt(); + } + if (installedFileName.compare(updateData["new_file_name"].toString(), + Qt::CaseInsensitive) == 0) { + return updateData["new_file_id"].toInt(); + } + } + return std::nullopt; +} - if (newModStatus != NexusInterface::FileStatus::OLD_VERSION && - newModStatus != NexusInterface::FileStatus::REMOVED && - newModStatus != NexusInterface::FileStatus::ARCHIVED) { +/** + * @brief Pick the newest version string for an installed file by walking + * the update chain. Active files prefer an active successor; obsolete + * files also accept the latest downloadable (listed, not removed) + * successor; both fall back to the file's own version. + */ +QString pickNewestVersion(int installedFileId, bool fileIsActive, + const QHash<int, QVariantMap>& filesById, + const QHash<int, QVariantMap>& updatesByOldId) +{ + const auto chainSuccessors = + findUpdateChainSuccessors(installedFileId, updatesByOldId); - // since the file is still active if there are no updates for it, use this - // as current version - validNewVersion = foundFileData["version"].toString(); - } - break; - } - } + std::optional<int> latestActiveSuccessor; + std::optional<int> latestDownloadableSuccessor; - if (foundFileData.isEmpty()) { - // The file was not listed, the file is likely archived and archived files are - // being hidden on the mod - newModStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; - } + for (auto it = chainSuccessors.rbegin(); it != chainSuccessors.rend(); ++it) { + const int successorId = *it; + const auto succIt = filesById.constFind(successorId); - // look for updates of the file - int currentUpdateId = -1; + if (succIt == filesById.constEnd()) { + // Not in files list — effectively archived and hidden by the author. + continue; + } - // find installed file ID from the updates list since old filenames are not - // guaranteed to be unique - for (const auto& updateEntry : fileUpdates) { - const QVariantMap& updateData = updateEntry.toMap(); + const int succStatus = succIt.value()["category_id"].toInt(); - if (installedFile.compare(updateData["old_file_name"].toString(), - Qt::CaseInsensitive) == 0) { - currentUpdateId = updateData["old_file_id"].toInt(); - break; - } - } + if (NexusInterface::isActiveFileStatus(succStatus)) { + latestActiveSuccessor = successorId; + break; + } - bool foundActiveUpdate = false; + if (!latestDownloadableSuccessor && + succStatus != NexusInterface::FileStatus::REMOVED) { + latestDownloadableSuccessor = successorId; + } + } - // there is at least one update - if (currentUpdateId > 0) { - bool lookForMoreUpdates = true; + std::optional<int> versionSourceId; + if (latestActiveSuccessor) { + versionSourceId = latestActiveSuccessor; + } else if (!fileIsActive && latestDownloadableSuccessor) { + versionSourceId = latestDownloadableSuccessor; + } else { + versionSourceId = installedFileId; + } - // follow the update chain until there are no more updates - while (lookForMoreUpdates) { - lookForMoreUpdates = false; + return filesById.value(*versionSourceId)["version"].toString(); +} - for (const auto& updateEntry : fileUpdates) { - const QVariantMap& updateData = updateEntry.toMap(); +} // namespace - if (currentUpdateId == updateData["old_file_id"].toInt()) { - currentUpdateId = updateData["new_file_id"].toInt(); +void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, + QVariant resultData, int requestID) +{ + QVariantMap resultInfo = resultData.toMap(); + QList const files = resultInfo["files"].toList(); + QList const fileUpdates = resultInfo["file_updates"].toList(); - // check if the new file is still active - for (const auto& file : files) { - const QVariantMap& fileData = file.toMap(); + QString gameShortName; + for (IPluginGame* game : m_PluginContainer.plugins<IPluginGame>()) { + if (game->gameNexusName() == gameName) { + gameShortName = game->gameShortName(); + break; + } + } - if (currentUpdateId == fileData["file_id"].toInt()) { - int const updateStatus = fileData["category_id"].toInt(); + QHash<int, QVariantMap> filesById; + filesById.reserve(files.size()); + for (const auto& file : files) { + const QVariantMap fileData = file.toMap(); + filesById.insert(fileData["file_id"].toInt(), fileData); + } - if (updateStatus != NexusInterface::FileStatus::OLD_VERSION && - updateStatus != NexusInterface::FileStatus::REMOVED && - updateStatus != NexusInterface::FileStatus::ARCHIVED) { + QHash<int, QVariantMap> updatesByOldId; + updatesByOldId.reserve(fileUpdates.size()); + for (const auto& updateEntry : fileUpdates) { + const QVariantMap updateData = updateEntry.toMap(); + updatesByOldId.insert(updateData["old_file_id"].toInt(), updateData); + } - // new version is active, so record it - validNewVersion = fileData["version"].toString(); - foundActiveUpdate = true; - } - break; - } - } + bool requiresInfo = false; + std::vector<ModInfo::Ptr> const installedMods = ModInfo::getByModID(gameShortName, modID); - lookForMoreUpdates = true; - break; - } - } - } - } + for (const auto& mod : installedMods) { + mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - // if there were no active direct file updates for the installedFile - if (!foundActiveUpdate) { - // get the global mod version in case the file isn't an optional - if (newModStatus != NexusInterface::FileStatus::OPTIONAL_FILE && - newModStatus != NexusInterface::FileStatus::MISCELLANEOUS) { - requiresInfo = true; - } - } - } else { - // No installedFile means we don't know what to look at for a version so - // just get the global mod version + const auto installedFileId = resolveInstalledFileId(mod, files, fileUpdates); + if (!installedFileId) { + // Manually-created mod (modID set via the edit dialog) or anything we + // can't tie back to a Nexus file by filename. Fall back to the global + // mod page version. requiresInfo = true; + continue; } - if (newModStatus > 0) { - mod->setNexusFileStatus(newModStatus); + int nexusFileStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; + if (const auto fileIt = filesById.constFind(*installedFileId); + fileIt != filesById.constEnd()) { + nexusFileStatus = fileIt.value()["category_id"].toInt(); } + mod->setNexusFileStatus(nexusFileStatus); - if (!validNewVersion.isEmpty()) { - mod->setNewestVersion(validNewVersion); - mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); + const QString newestVersionValue = pickNewestVersion( + *installedFileId, NexusInterface::isActiveFileStatus(nexusFileStatus), + filesById, updatesByOldId); + if (!newestVersionValue.isEmpty()) { + mod->setNewestVersion(newestVersionValue); } } @@ -3524,7 +3580,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD ui->modList->invalidateFilter(); if (requiresInfo) { - NexusInterface::instance().requestModInfo(gameNameReal, modID, this, QVariant(), + NexusInterface::instance().requestModInfo(gameShortName, modID, this, QVariant(), QString()); } } diff --git a/src/src/mainwindow.h b/src/src/mainwindow.h index 9af07bd..6443862 100644 --- a/src/src/mainwindow.h +++ b/src/src/mainwindow.h @@ -132,6 +132,10 @@ public: QMainWindow* mainWindow() override;
+ void showNotification(const QString& title, const QString& message,
+ QSystemTrayIcon::MessageIcon icon =
+ QSystemTrayIcon::MessageIcon::Information) override;
+
bool addProfile();
void updateBSAList(const QStringList& defaultArchives,
const QStringList& activeArchives) override;
diff --git a/src/src/nexusinterface.h b/src/src/nexusinterface.h index e8e2798..1fa8792 100644 --- a/src/src/nexusinterface.h +++ b/src/src/nexusinterface.h @@ -183,6 +183,31 @@ public: // listed we can assume they were hidden.
};
+ /**
+ * @brief Whether a Nexus file category represents a file that is still
+ * listed and considered current on the mod page.
+ *
+ * Inactive (false) refers to files that have been marked as obsolete or have been
+ * removed.
+ */
+ static bool isActiveFileStatus(int status)
+ {
+ switch (status) {
+ case FileStatus::MAIN:
+ case FileStatus::UPDATE:
+ case FileStatus::OPTIONAL_FILE:
+ case FileStatus::MISCELLANEOUS:
+ return true;
+ case FileStatus::OLD_VERSION:
+ case FileStatus::REMOVED:
+ case FileStatus::ARCHIVED:
+ case FileStatus::ARCHIVED_HIDDEN:
+ return false;
+ default:
+ return false;
+ }
+ }
+
public:
static APILimits defaultAPILimits();
static APILimits parseLimits(const QNetworkReply* reply);
diff --git a/src/src/nxmaccessmanager.cpp b/src/src/nxmaccessmanager.cpp index 9e00401..e5d2e71 100644 --- a/src/src/nxmaccessmanager.cpp +++ b/src/src/nxmaccessmanager.cpp @@ -31,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QCoreApplication> #include <QDir> #include <QEventLoop> +#include <QFile> #include <QJsonArray> #include <QJsonDocument> #include <QMessageBox> @@ -329,7 +330,7 @@ void ValidationAttempt::onFinished() const QString id = data.value("sub").toString(); const QString name = data.value("name").toString(); const auto roles = data.value("membership_roles").toArray(); - QStringList validRoles = {"premium", "lifetimepremium", "supporter"}; + QStringList validRoles = {"premium", "lifetimepremium"}; bool premium = false; for (auto role : roles) { QString roleVal = role.toString(); @@ -982,11 +983,15 @@ bool NXMAccessManager::validateWaiting() const void NXMAccessManager::connectOrRefresh(const NexusOAuthTokens tokens) { + if (m_NexusOAuth->status() != QAbstractOAuth::Status::NotAuthenticated && + m_NexusOAuth->status() != QAbstractOAuth::Status::Granted) + return; const auto clientId = NexusOAuth::clientId(); if (clientId.isEmpty()) { handleOAuthError(QObject::tr("No OAuth client id configured.")); return; } + m_ValidationState = NotChecked; m_NexusOAuth->setAuthorizationUrl(QUrl(NexusOAuth::authorizeUrl())); m_NexusOAuth->setTokenUrl(QUrl(NexusOAuth::tokenUrl())); m_NexusOAuth->setClientIdentifier(clientId); @@ -995,10 +1000,27 @@ void NXMAccessManager::connectOrRefresh(const NexusOAuthTokens tokens) m_NexusOAuth->setRequestedScopeTokens(scope); m_NexusOAuthReplyHandler->close(); m_NexusOAuthReplyHandler->setCallbackPath(QUrl(NexusOAuth::redirectUri()).path()); + QFile logo(":/MO/gui/app_icon"); + logo.open(QIODevice::ReadOnly); + QByteArray imageData = logo.readAll(); + logo.close(); + QByteArray base64Data = imageData.toBase64(); + QString imageSrc = + QString("data:image/png;base64,") + QString::fromLatin1(base64Data); m_NexusOAuthReplyHandler->setCallbackText( - QObject::tr("<html><body><h2>Mod Organizer</h2><p>Authorization complete. You " - "may close this " - "window.</p></body></html>")); + QString("<style>\n" + " body {\n" + " text-align: center;\n" + " background-color: #2b2b2b;\n" + " color: white;\n" + " font-family: sans-serif;\n" + " font-size: 18px;\n" + " }\n" + "</style>\n" + "<img src=\"%1\" alt=\"Mod Organizer\">\n") + .arg(imageSrc) + + QObject::tr("<p><strong>Authorization complete.<br>You may close this " + "window.</strong></p>\n")); if (!m_NexusOAuthReplyHandler->listen(QHostAddress::LocalHost, NexusOAuth::redirectPort())) { handleOAuthError(QObject::tr("Failed to bind to localhost on port %1.") diff --git a/src/src/organizer_en.ts b/src/src/organizer_en.ts index 0575bee..abcf2fd 100644 --- a/src/src/organizer_en.ts +++ b/src/src/organizer_en.ts @@ -710,42 +710,37 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1103"/> - <source>Enter API Key Manually</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="createinstancedialog.ui" line="1154"/> + <location filename="createinstancedialog.ui" line="1147"/> <source><h3>Confirmation</h3></source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1161"/> + <location filename="createinstancedialog.ui" line="1154"/> <source>The instance is about to be created. Review the information below and press 'Finish'.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1202"/> + <location filename="createinstancedialog.ui" line="1195"/> <source>Instance creation log</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1237"/> + <location filename="createinstancedialog.ui" line="1230"/> <source>Launch the new instance</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1285"/> + <location filename="createinstancedialog.ui" line="1278"/> <source>< Back</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1292"/> + <location filename="createinstancedialog.ui" line="1285"/> <source>Next ></source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialog.ui" line="1302"/> + <location filename="createinstancedialog.ui" line="1295"/> <source>Cancel</source> <translation type="unfinished"></translation> </message> @@ -1008,145 +1003,150 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="downloadlistview.cpp" line="242"/> + <source>Visit the uploader's profile</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadlistview.cpp" line="246"/> <source>Open File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="245"/> + <location filename="downloadlistview.cpp" line="249"/> <source>Open Meta File</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="248"/> - <location filename="downloadlistview.cpp" line="272"/> - <location filename="downloadlistview.cpp" line="284"/> + <location filename="downloadlistview.cpp" line="252"/> + <location filename="downloadlistview.cpp" line="276"/> + <location filename="downloadlistview.cpp" line="288"/> <source>Reveal in Explorer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="254"/> - <location filename="downloadlistview.cpp" line="278"/> + <location filename="downloadlistview.cpp" line="258"/> + <location filename="downloadlistview.cpp" line="282"/> <source>Delete...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="258"/> + <location filename="downloadlistview.cpp" line="262"/> <source>Un-Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="262"/> + <location filename="downloadlistview.cpp" line="266"/> <source>Hide</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="266"/> - <location filename="downloadlistview.cpp" line="380"/> + <location filename="downloadlistview.cpp" line="270"/> + <location filename="downloadlistview.cpp" line="384"/> <source>Cancel</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="269"/> + <location filename="downloadlistview.cpp" line="273"/> <source>Pause</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="281"/> + <location filename="downloadlistview.cpp" line="285"/> <source>Resume</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="296"/> + <location filename="downloadlistview.cpp" line="300"/> <source>Delete Installed Downloads...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="299"/> + <location filename="downloadlistview.cpp" line="303"/> <source>Delete Uninstalled Downloads...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="302"/> + <location filename="downloadlistview.cpp" line="306"/> <source>Delete All Downloads...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="308"/> + <location filename="downloadlistview.cpp" line="312"/> <source>Hide Installed...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="311"/> + <location filename="downloadlistview.cpp" line="315"/> <source>Hide Uninstalled...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="314"/> + <location filename="downloadlistview.cpp" line="318"/> <source>Hide All...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="318"/> + <location filename="downloadlistview.cpp" line="322"/> <source>Un-Hide All...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="375"/> + <location filename="downloadlistview.cpp" line="379"/> <source>Delete download</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="379"/> + <location filename="downloadlistview.cpp" line="383"/> <source>Move to the Recycle Bin</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="444"/> - <location filename="downloadlistview.cpp" line="455"/> - <location filename="downloadlistview.cpp" line="466"/> + <location filename="downloadlistview.cpp" line="453"/> + <location filename="downloadlistview.cpp" line="464"/> + <location filename="downloadlistview.cpp" line="475"/> <source>Delete Files?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="445"/> + <location filename="downloadlistview.cpp" line="454"/> <source>This will remove all finished downloads from this list and from disk. Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="456"/> + <location filename="downloadlistview.cpp" line="465"/> <source>This will remove all installed downloads from this list and from disk. Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="467"/> + <location filename="downloadlistview.cpp" line="476"/> <source>This will remove all uninstalled downloads from this list and from disk. Are you absolutely sure you want to proceed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="476"/> - <location filename="downloadlistview.cpp" line="486"/> - <location filename="downloadlistview.cpp" line="496"/> + <location filename="downloadlistview.cpp" line="485"/> + <location filename="downloadlistview.cpp" line="495"/> + <location filename="downloadlistview.cpp" line="505"/> <source>Hide Files?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="477"/> + <location filename="downloadlistview.cpp" line="486"/> <source>This will remove all finished downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="487"/> + <location filename="downloadlistview.cpp" line="496"/> <source>This will remove all installed downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadlistview.cpp" line="497"/> + <location filename="downloadlistview.cpp" line="506"/> <source>This will remove all uninstalled downloads from this list (but NOT from disk).</source> <translation type="unfinished"></translation> </message> @@ -1154,22 +1154,22 @@ Are you absolutely sure you want to proceed?</source> <context> <name>DownloadManager</name> <message> - <location filename="downloadmanager.cpp" line="198"/> + <location filename="downloadmanager.cpp" line="201"/> <source>failed to rename "%1" to "%2"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="440"/> + <location filename="downloadmanager.cpp" line="443"/> <source>Memory allocation error (in refreshing directory).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="455"/> + <location filename="downloadmanager.cpp" line="458"/> <source>Query Metadata</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="456"/> + <location filename="downloadmanager.cpp" line="459"/> <source>There are %1 downloads with incomplete metadata. Do you want to fetch all incomplete metadata? @@ -1177,32 +1177,32 @@ API requests will be consumed, and Mod Organizer may stutter.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="606"/> + <location filename="downloadmanager.cpp" line="609"/> <source>failed to download %1: could not open output file: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="635"/> + <location filename="downloadmanager.cpp" line="638"/> <source>Download again?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="636"/> + <location filename="downloadmanager.cpp" line="639"/> <source>A file with the same name "%1" has already been downloaded. Do you want to download it again? The new file will receive a different name.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="703"/> + <location filename="downloadmanager.cpp" line="706"/> <source>Wrong Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="704"/> + <location filename="downloadmanager.cpp" line="707"/> <source>The download link is for a mod for "%1" but this instance of MO has been set up for "%2".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="718"/> + <location filename="downloadmanager.cpp" line="721"/> <source>There is already a download queued for this file. Mod %1 @@ -1210,12 +1210,12 @@ File %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="725"/> + <location filename="downloadmanager.cpp" line="728"/> <source>Already Queued</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="738"/> + <location filename="downloadmanager.cpp" line="741"/> <source>There is already a download started for this file. Mod %1: %2 @@ -1223,287 +1223,297 @@ File %3: %4</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="774"/> + <location filename="downloadmanager.cpp" line="777"/> <source>Already Started</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="806"/> - <location filename="downloadmanager.cpp" line="936"/> + <location filename="downloadmanager.cpp" line="809"/> + <location filename="downloadmanager.cpp" line="939"/> <source>remove: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="824"/> + <location filename="downloadmanager.cpp" line="827"/> <source>failed to delete %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="830"/> + <location filename="downloadmanager.cpp" line="833"/> <source>failed to delete meta file for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="889"/> + <location filename="downloadmanager.cpp" line="892"/> <source>restore: invalid download index: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="955"/> + <location filename="downloadmanager.cpp" line="958"/> <source>cancel: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="967"/> + <location filename="downloadmanager.cpp" line="970"/> <source>pause: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="989"/> + <location filename="downloadmanager.cpp" line="992"/> <source>resume: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1000"/> + <location filename="downloadmanager.cpp" line="1003"/> <source>resume (int): invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1025"/> + <location filename="downloadmanager.cpp" line="1028"/> <source>No known download urls. Sorry, this download can't be resumed.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1068"/> - <location filename="downloadmanager.cpp" line="1133"/> + <location filename="downloadmanager.cpp" line="1071"/> + <location filename="downloadmanager.cpp" line="1136"/> <source>query: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1090"/> + <location filename="downloadmanager.cpp" line="1093"/> <source>Please enter the Nexus mod ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1091"/> + <location filename="downloadmanager.cpp" line="1094"/> <source>Mod ID:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1109"/> + <location filename="downloadmanager.cpp" line="1112"/> <source>Please select the source game code for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1167"/> + <location filename="downloadmanager.cpp" line="1170"/> <source>Hashing download file '%1'</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1168"/> + <location filename="downloadmanager.cpp" line="1171"/> <source>Cancel</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1196"/> + <location filename="downloadmanager.cpp" line="1199"/> <source>VisitNexus: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1216"/> + <location filename="downloadmanager.cpp" line="1219"/> <source>Nexus ID for this Mod is unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1223"/> + <location filename="downloadmanager.cpp" line="1226"/> + <source>VisitUploaderProfile: invalid download index %1</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadmanager.cpp" line="1240"/> + <source>Uploader for this Mod is unknown</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="downloadmanager.cpp" line="1247"/> <source>OpenFile: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1267"/> + <location filename="downloadmanager.cpp" line="1291"/> <source>OpenFileInDownloadsFolder: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1300"/> + <location filename="downloadmanager.cpp" line="1324"/> <source>get pending: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1309"/> + <location filename="downloadmanager.cpp" line="1333"/> <source>get path: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1319"/> + <location filename="downloadmanager.cpp" line="1343"/> <source>Main</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1321"/> + <location filename="downloadmanager.cpp" line="1345"/> <source>Update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1323"/> + <location filename="downloadmanager.cpp" line="1347"/> <source>Optional</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1325"/> + <location filename="downloadmanager.cpp" line="1349"/> <source>Old</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1327"/> + <location filename="downloadmanager.cpp" line="1351"/> <source>Miscellaneous</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1329"/> + <location filename="downloadmanager.cpp" line="1353"/> <source>Deleted</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1331"/> + <location filename="downloadmanager.cpp" line="1355"/> <source>Archived</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1333"/> + <location filename="downloadmanager.cpp" line="1357"/> <source>Unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1340"/> + <location filename="downloadmanager.cpp" line="1364"/> <source>display name: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1361"/> + <location filename="downloadmanager.cpp" line="1385"/> <source>file name: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1370"/> + <location filename="downloadmanager.cpp" line="1394"/> <source>file time: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1389"/> + <location filename="downloadmanager.cpp" line="1413"/> <source>file size: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1398"/> + <location filename="downloadmanager.cpp" line="1422"/> <source>progress: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1407"/> + <location filename="downloadmanager.cpp" line="1431"/> <source>state: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1416"/> + <location filename="downloadmanager.cpp" line="1440"/> <source>infocomplete: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1430"/> - <location filename="downloadmanager.cpp" line="1438"/> - <location filename="downloadmanager.cpp" line="1451"/> + <location filename="downloadmanager.cpp" line="1454"/> + <location filename="downloadmanager.cpp" line="1462"/> + <location filename="downloadmanager.cpp" line="1475"/> <source>mod id: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1459"/> + <location filename="downloadmanager.cpp" line="1483"/> <source>ishidden: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1467"/> + <location filename="downloadmanager.cpp" line="1491"/> <source>file info: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1476"/> + <location filename="downloadmanager.cpp" line="1500"/> <source>mark installed: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1517"/> + <location filename="downloadmanager.cpp" line="1541"/> <source>mark uninstalled: invalid download index %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1682"/> + <location filename="downloadmanager.cpp" line="1706"/> <source>%1% - %2 - ~%3</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1693"/> + <location filename="downloadmanager.cpp" line="1717"/> <source>Memory allocation error (in processing progress event).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1702"/> + <location filename="downloadmanager.cpp" line="1726"/> <source>Memory allocation error (in processing downloaded data).</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1823"/> + <location filename="downloadmanager.cpp" line="1856"/> <source>Information updated</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1825"/> - <location filename="downloadmanager.cpp" line="1848"/> + <location filename="downloadmanager.cpp" line="1858"/> + <location filename="downloadmanager.cpp" line="1881"/> <source>No matching file found on Nexus! Maybe this file is no longer available or it was renamed?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="1828"/> + <location filename="downloadmanager.cpp" line="1861"/> <source>No file on Nexus matches the selected file by name. Please manually choose the correct one.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2024"/> + <location filename="downloadmanager.cpp" line="2057"/> <source>No download server available. Please try again later.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2199"/> + <location filename="downloadmanager.cpp" line="2235"/> <source>Failed to request file info from nexus: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2231"/> + <location filename="downloadmanager.cpp" line="2267"/> <source>Warning: Content type is: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2238"/> + <location filename="downloadmanager.cpp" line="2274"/> <source>Download header content length: %1 downloaded file size: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2242"/> + <location filename="downloadmanager.cpp" line="2278"/> <source>Download failed: %1 (%2)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2267"/> + <location filename="downloadmanager.cpp" line="2303"/> <source>We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2358"/> + <location filename="downloadmanager.cpp" line="2394"/> <source>failed to re-open %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadmanager.cpp" line="2406"/> + <location filename="downloadmanager.cpp" line="2442"/> <source>Unable to write download to drive (return %1). Check the drive's available storage. @@ -1514,12 +1524,12 @@ Canceling download "%2"...</source> <context> <name>DownloadsTab</name> <message> - <location filename="downloadstab.cpp" line="90"/> + <location filename="downloadstab.cpp" line="92"/> <source>Query Metadata</source> <translation type="unfinished"></translation> </message> <message> - <location filename="downloadstab.cpp" line="91"/> + <location filename="downloadstab.cpp" line="93"/> <source>Cannot query metadata while offline mode is enabled. Do you want to disable offline mode?</source> <translation type="unfinished"></translation> </message> @@ -2393,12 +2403,12 @@ This is likely due to a corrupted or incompatible download or unrecognized archi </message> <message> <location filename="installationmanager.cpp" line="882"/> - <source>7z.so not found</source> + <source>7z.dll not found</source> <translation type="unfinished"></translation> </message> <message> <location filename="installationmanager.cpp" line="885"/> - <source>7z.so isn't valid</source> + <source>7z.dll isn't valid</source> <translation type="unfinished"></translation> </message> <message> @@ -3896,120 +3906,120 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3400"/> + <location filename="mainwindow.cpp" line="3406"/> <source>Thank you!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3401"/> + <location filename="mainwindow.cpp" line="3407"/> <source>Thank you for your endorsement!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3515"/> + <location filename="mainwindow.cpp" line="3521"/> <source>Mod ID %1 no longer seems to be available on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3533"/> + <location filename="mainwindow.cpp" line="3539"/> <source>Error %1: Request to Nexus failed: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3550"/> - <location filename="mainwindow.cpp" line="3623"/> + <location filename="mainwindow.cpp" line="3556"/> + <location filename="mainwindow.cpp" line="3629"/> <source>failed to read %1: %2</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3563"/> + <location filename="mainwindow.cpp" line="3569"/> <source>Error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3564"/> + <location filename="mainwindow.cpp" line="3570"/> <source>failed to extract %1 (errorcode %2)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3598"/> + <location filename="mainwindow.cpp" line="3604"/> <source>Extract BSA</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3637"/> + <location filename="mainwindow.cpp" line="3643"/> <source>This archive contains invalid hashes. Some files may be broken.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3647"/> + <location filename="mainwindow.cpp" line="3653"/> <source>Extract...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3716"/> + <location filename="mainwindow.cpp" line="3722"/> <source>Remove '%1' from the toolbar</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3772"/> + <location filename="mainwindow.cpp" line="3778"/> <source>Backup of load order created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3783"/> + <location filename="mainwindow.cpp" line="3789"/> <source>Choose backup to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3800"/> + <location filename="mainwindow.cpp" line="3806"/> <source>This file might be left over following a crash or power loss event. Check its contents before restoring.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3809"/> + <location filename="mainwindow.cpp" line="3815"/> <source>No Backups</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3810"/> + <location filename="mainwindow.cpp" line="3816"/> <source>There are no backups to restore</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3834"/> - <location filename="mainwindow.cpp" line="3858"/> + <location filename="mainwindow.cpp" line="3840"/> + <location filename="mainwindow.cpp" line="3864"/> <source>Restore failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3835"/> - <location filename="mainwindow.cpp" line="3859"/> + <location filename="mainwindow.cpp" line="3841"/> + <location filename="mainwindow.cpp" line="3865"/> <source>Failed to restore the backup. Errorcode: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3847"/> + <location filename="mainwindow.cpp" line="3853"/> <source>Backup of mod list created</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3927"/> + <location filename="mainwindow.cpp" line="3933"/> <source>A file with the same name has already been downloaded. What would you like to do?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3929"/> + <location filename="mainwindow.cpp" line="3935"/> <source>Overwrite</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3930"/> + <location filename="mainwindow.cpp" line="3936"/> <source>Rename new file</source> <translation type="unfinished"></translation> </message> <message> - <location filename="mainwindow.cpp" line="3931"/> + <location filename="mainwindow.cpp" line="3937"/> <source>Ignore file</source> <translation type="unfinished"></translation> </message> @@ -4478,12 +4488,12 @@ p, li { white-space: pre-wrap; } <context> <name>ModInfoRegular</name> <message> - <location filename="modinforegular.cpp" line="736"/> + <location filename="modinforegular.cpp" line="742"/> <source>%1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modinforegular.cpp" line="742"/> + <location filename="modinforegular.cpp" line="748"/> <source>Categories: <br></source> <translation type="unfinished"></translation> </message> @@ -4584,178 +4594,198 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="263"/> + <location filename="modlist.cpp" line="267"/> <source>invalid</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="409"/> + <location filename="modlist.cpp" line="413"/> <source>installed version: "%1", newest version: "%2"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="414"/> + <location filename="modlist.cpp" line="418"/> <source>The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade".</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="422"/> + <location filename="modlist.cpp" line="426"/> <source>This file has been marked as "Old". There is most likely an updated version of this file available.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="430"/> + <location filename="modlist.cpp" line="434"/> <source>This file has been marked as "Deleted"! You may want to check for an update or remove the nexus ID from this mod!</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="440"/> + <location filename="modlist.cpp" line="444"/> <source>%1 minute(s) and %2 second(s)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="442"/> + <location filename="modlist.cpp" line="446"/> <source>This mod will be available to check in %2.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="449"/> + <location filename="modlist.cpp" line="453"/> <source>Categories: <br></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="482"/> + <location filename="modlist.cpp" line="486"/> <source>Invalid name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="488"/> + <location filename="modlist.cpp" line="492"/> <source>Name is already in use by another mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1225"/> + <location filename="modlist.cpp" line="1229"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1226"/> + <location filename="modlist.cpp" line="1230"/> <source>Are you sure you want to remove "%1"?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1307"/> + <location filename="modlist.cpp" line="1311"/> <source>Conflicts</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1309"/> + <location filename="modlist.cpp" line="1313"/> <source>Flags</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1311"/> + <location filename="modlist.cpp" line="1315"/> <source>Content</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1313"/> + <location filename="modlist.cpp" line="1317"/> <source>Mod Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1315"/> + <location filename="modlist.cpp" line="1319"/> <source>Version</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1317"/> + <location filename="modlist.cpp" line="1321"/> <source>Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1319"/> + <location filename="modlist.cpp" line="1323"/> <source>Category</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1321"/> + <location filename="modlist.cpp" line="1325"/> + <source>Author</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlist.cpp" line="1327"/> + <source>Uploader</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlist.cpp" line="1329"/> <source>Source Game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1323"/> + <location filename="modlist.cpp" line="1331"/> <source>Nexus ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1325"/> + <location filename="modlist.cpp" line="1333"/> <source>Installation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1327"/> + <location filename="modlist.cpp" line="1335"/> <source>Notes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1329"/> - <location filename="modlist.cpp" line="1373"/> + <location filename="modlist.cpp" line="1337"/> + <location filename="modlist.cpp" line="1385"/> <source>unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1337"/> + <location filename="modlist.cpp" line="1345"/> <source>Name of your mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1339"/> + <location filename="modlist.cpp" line="1347"/> <source>Version of the mod (if available)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1341"/> + <location filename="modlist.cpp" line="1349"/> <source>Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1345"/> + <location filename="modlist.cpp" line="1353"/> <source>Primary category of the mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1347"/> + <location filename="modlist.cpp" line="1355"/> + <source>Author(s) of the mod.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlist.cpp" line="1357"/> + <source>Uploader of the mod. This is not necessarily the same as the author.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlist.cpp" line="1359"/> <source>The source game which was the origin of this mod.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1349"/> + <location filename="modlist.cpp" line="1361"/> <source>Id of the mod as used on Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1351"/> + <location filename="modlist.cpp" line="1363"/> <source>Indicators of file conflicts between mods.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1353"/> + <location filename="modlist.cpp" line="1365"/> <source>Emblems to highlight things that might require attention.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1360"/> + <location filename="modlist.cpp" line="1372"/> <source>Depicts the content of the mod:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1369"/> + <location filename="modlist.cpp" line="1381"/> <source>Time this mod was installed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlist.cpp" line="1371"/> + <location filename="modlist.cpp" line="1383"/> <source>User notes about the mod</source> <translation type="unfinished"></translation> </message> @@ -4852,8 +4882,8 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="modlistcontextmenu.cpp" line="376"/> - <location filename="modlistcontextmenu.cpp" line="453"/> - <location filename="modlistcontextmenu.cpp" line="622"/> + <location filename="modlistcontextmenu.cpp" line="459"/> + <location filename="modlistcontextmenu.cpp" line="634"/> <source>Open in Explorer</source> <translation type="unfinished"></translation> </message> @@ -4869,13 +4899,13 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="modlistcontextmenu.cpp" line="398"/> - <location filename="modlistcontextmenu.cpp" line="525"/> + <location filename="modlistcontextmenu.cpp" line="531"/> <source>Select Color...</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistcontextmenu.cpp" line="403"/> - <location filename="modlistcontextmenu.cpp" line="529"/> + <location filename="modlistcontextmenu.cpp" line="535"/> <source>Reset Color</source> <translation type="unfinished"></translation> </message> @@ -4891,121 +4921,127 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="modlistcontextmenu.cpp" line="429"/> - <location filename="modlistcontextmenu.cpp" line="595"/> + <location filename="modlistcontextmenu.cpp" line="601"/> <source>Ignore missing data</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistcontextmenu.cpp" line="435"/> - <location filename="modlistcontextmenu.cpp" line="602"/> + <location filename="modlistcontextmenu.cpp" line="608"/> <source>Mark as converted/working</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistcontextmenu.cpp" line="441"/> - <location filename="modlistcontextmenu.cpp" line="610"/> + <location filename="modlistcontextmenu.cpp" line="616"/> <source>Visit on Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="448"/> - <location filename="modlistcontextmenu.cpp" line="617"/> + <location filename="modlistcontextmenu.cpp" line="447"/> + <location filename="modlistcontextmenu.cpp" line="622"/> + <source>Visit the uploader's profile</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlistcontextmenu.cpp" line="454"/> + <location filename="modlistcontextmenu.cpp" line="629"/> <source>Visit on %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="466"/> + <location filename="modlistcontextmenu.cpp" line="472"/> <source>Change versioning scheme</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="472"/> + <location filename="modlistcontextmenu.cpp" line="478"/> <source>Force-check updates</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="476"/> + <location filename="modlistcontextmenu.cpp" line="482"/> <source>Un-ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="481"/> + <location filename="modlistcontextmenu.cpp" line="487"/> <source>Ignore update</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="488"/> + <location filename="modlistcontextmenu.cpp" line="494"/> <source>Enable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="491"/> + <location filename="modlistcontextmenu.cpp" line="497"/> <source>Disable selected</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="502"/> + <location filename="modlistcontextmenu.cpp" line="508"/> <source>Rename Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="505"/> + <location filename="modlistcontextmenu.cpp" line="511"/> <source>Reinstall Mod</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="508"/> + <location filename="modlistcontextmenu.cpp" line="514"/> <source>Remove Mod...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="511"/> + <location filename="modlistcontextmenu.cpp" line="517"/> <source>Create Backup</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="517"/> + <location filename="modlistcontextmenu.cpp" line="523"/> <source>Restore hidden files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="539"/> + <location filename="modlistcontextmenu.cpp" line="545"/> <source>Un-Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="544"/> - <location filename="modlistcontextmenu.cpp" line="552"/> + <location filename="modlistcontextmenu.cpp" line="550"/> + <location filename="modlistcontextmenu.cpp" line="558"/> <source>Endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="547"/> + <location filename="modlistcontextmenu.cpp" line="553"/> <source>Won't endorse</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="557"/> + <location filename="modlistcontextmenu.cpp" line="563"/> <source>Endorsement state unknown</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="567"/> + <location filename="modlistcontextmenu.cpp" line="573"/> <source>Remap Category (From Nexus)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="575"/> + <location filename="modlistcontextmenu.cpp" line="581"/> <source>Start tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="580"/> + <location filename="modlistcontextmenu.cpp" line="586"/> <source>Stop tracking</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistcontextmenu.cpp" line="585"/> + <location filename="modlistcontextmenu.cpp" line="591"/> <source>Tracked state unknown</source> <translation type="unfinished"></translation> </message> @@ -5124,7 +5160,7 @@ p, li { white-space: pre-wrap; } <context> <name>ModListSortProxy</name> <message> - <location filename="modlistsortproxy.cpp" line="629"/> + <location filename="modlistsortproxy.cpp" line="661"/> <source>Drag&Drop is only supported when sorting by priority</source> <translation type="unfinished"></translation> </message> @@ -5153,17 +5189,17 @@ Please enter the name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistview.cpp" line="974"/> + <location filename="modlistview.cpp" line="975"/> <source>Exception: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistview.cpp" line="976"/> + <location filename="modlistview.cpp" line="977"/> <source>Unknown exception</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistview.cpp" line="1342"/> + <location filename="modlistview.cpp" line="1343"/> <source><Multiple></source> <translation type="unfinished"></translation> </message> @@ -5182,7 +5218,7 @@ Please enter the name:</source> </message> <message> <location filename="modlistviewactions.cpp" line="120"/> - <location filename="modlistviewactions.cpp" line="1360"/> + <location filename="modlistviewactions.cpp" line="1400"/> <source>Create Mod...</source> <translation type="unfinished"></translation> </message> @@ -5194,7 +5230,7 @@ Please enter a name:</source> </message> <message> <location filename="modlistviewactions.cpp" line="131"/> - <location filename="modlistviewactions.cpp" line="1371"/> + <location filename="modlistviewactions.cpp" line="1411"/> <source>A mod with this name already exists</source> <translation type="unfinished"></translation> </message> @@ -5226,8 +5262,8 @@ Please enter a name:</source> </message> <message> <location filename="modlistviewactions.cpp" line="217"/> - <location filename="modlistviewactions.cpp" line="795"/> - <location filename="modlistviewactions.cpp" line="1044"/> + <location filename="modlistviewactions.cpp" line="816"/> + <location filename="modlistviewactions.cpp" line="1084"/> <source>Confirm</source> <translation type="unfinished"></translation> </message> @@ -5239,8 +5275,8 @@ Please enter a name:</source> </message> <message> <location filename="modlistviewactions.cpp" line="267"/> - <location filename="modlistviewactions.cpp" line="1078"/> - <location filename="modlistviewactions.cpp" line="1430"/> + <location filename="modlistviewactions.cpp" line="1118"/> + <location filename="modlistviewactions.cpp" line="1470"/> <source>Are you sure?</source> <translation type="unfinished"></translation> </message> @@ -5317,178 +5353,197 @@ You can also use online editors and converters instead.</source> </message> <message> <location filename="modlistviewactions.cpp" line="381"/> - <source>Nexus_ID</source> + <source>Mod_Author</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistviewactions.cpp" line="382"/> - <source>Mod_Nexus_URL</source> + <source>Mod_Uploader</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistviewactions.cpp" line="383"/> - <source>Mod_Version</source> + <source>Nexus_ID</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistviewactions.cpp" line="384"/> - <source>Install_Date</source> + <source>Mod_Nexus_URL</source> <translation type="unfinished"></translation> </message> <message> <location filename="modlistviewactions.cpp" line="385"/> + <source>Mod_Uploader_URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlistviewactions.cpp" line="386"/> + <source>Mod_Version</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlistviewactions.cpp" line="387"/> + <source>Install_Date</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlistviewactions.cpp" line="388"/> <source>Download_File_Name</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="512"/> + <location filename="modlistviewactions.cpp" line="533"/> <source>export failed: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="566"/> + <location filename="modlistviewactions.cpp" line="587"/> <source>Failed to display overwrite dialog: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="629"/> + <location filename="modlistviewactions.cpp" line="650"/> <source>Set Priority</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="630"/> + <location filename="modlistviewactions.cpp" line="651"/> <source>Set the priority of the selected mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="760"/> + <location filename="modlistviewactions.cpp" line="781"/> <source>failed to rename mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="796"/> + <location filename="modlistviewactions.cpp" line="817"/> <source>Remove the following mods?<br><ul>%1</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="813"/> + <location filename="modlistviewactions.cpp" line="834"/> <source>failed to remove mod: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="841"/> + <location filename="modlistviewactions.cpp" line="862"/> <source>Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="842"/> + <location filename="modlistviewactions.cpp" line="863"/> <source>The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="868"/> + <location filename="modlistviewactions.cpp" line="889"/> <source>Sorry</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="869"/> + <location filename="modlistviewactions.cpp" line="890"/> <source>I don't know a versioning scheme where %1 is newer than %2.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="890"/> - <source>Opening Nexus Links</source> + <location filename="modlistviewactions.cpp" line="911"/> + <source>Nexus Links</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="891"/> - <source>You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?</source> + <location filename="modlistviewactions.cpp" line="931"/> + <location filename="modlistviewactions.cpp" line="949"/> + <source>Web Pages</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="914"/> - <location filename="modlistviewactions.cpp" line="936"/> - <source>Opening Web Pages</source> + <location filename="modlistviewactions.cpp" line="978"/> + <source>Uploader Profiles</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="915"/> - <location filename="modlistviewactions.cpp" line="937"/> - <source>You are trying to open %1 Web Pages. Are you sure you want to do this?</source> + <location filename="modlistviewactions.cpp" line="998"/> + <source>Opening %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="987"/> - <location filename="modlistviewactions.cpp" line="992"/> - <location filename="modlistviewactions.cpp" line="1003"/> + <location filename="modlistviewactions.cpp" line="999"/> + <source>You are trying to open %1 %2. Are you sure you want to do this?</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="modlistviewactions.cpp" line="1027"/> + <location filename="modlistviewactions.cpp" line="1032"/> + <location filename="modlistviewactions.cpp" line="1043"/> <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="988"/> + <location filename="modlistviewactions.cpp" line="1028"/> <source>Installation file no longer exists</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="993"/> + <location filename="modlistviewactions.cpp" line="1033"/> <source>Mods installed with old versions of MO can't be reinstalled in this way.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1003"/> + <location filename="modlistviewactions.cpp" line="1043"/> <source>Failed to create backup.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1045"/> + <location filename="modlistviewactions.cpp" line="1085"/> <source>Restore all hidden files in the following mods?<br><ul>%1</ul></source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1079"/> + <location filename="modlistviewactions.cpp" line="1119"/> <source>About to restore all hidden files in: </source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1112"/> + <location filename="modlistviewactions.cpp" line="1152"/> <source>Endorsing multiple mods will take a while. Please wait...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1279"/> + <location filename="modlistviewactions.cpp" line="1319"/> <source>Overwrite?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1280"/> + <location filename="modlistviewactions.cpp" line="1320"/> <source>This will replace the existing mod "%1". Continue?</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1284"/> + <location filename="modlistviewactions.cpp" line="1324"/> <source>failed to remove mod "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1290"/> + <location filename="modlistviewactions.cpp" line="1330"/> <source>failed to rename "%1" to "%2"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1342"/> + <location filename="modlistviewactions.cpp" line="1382"/> <source>Move successful.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1361"/> + <location filename="modlistviewactions.cpp" line="1401"/> <source>This will move all files from overwrite into a new, regular mod. Please enter a name:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="modlistviewactions.cpp" line="1431"/> + <location filename="modlistviewactions.cpp" line="1471"/> <source>About to recursively delete: </source> <translation type="unfinished"></translation> @@ -5510,123 +5565,81 @@ Please enter a name:</source> <context> <name>NexusConnectionUI</name> <message> - <location filename="settingsdialognexus.cpp" line="104"/> + <location filename="settingsdialognexus.cpp" line="49"/> <source>Connected.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="106"/> + <location filename="settingsdialognexus.cpp" line="51"/> <source>Not connected.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="162"/> + <location filename="settingsdialognexus.cpp" line="86"/> <source>Disconnected.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="176"/> - <source>Checking API key...</source> + <location filename="settingsdialognexus.cpp" line="100"/> + <source>Authorizing with Nexus...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="185"/> - <source>Received API key.</source> + <location filename="settingsdialognexus.cpp" line="107"/> + <source>Received authorization from Nexus.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="210"/> + <location filename="settingsdialognexus.cpp" line="131"/> <source>Received user account information</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="213"/> + <location filename="settingsdialognexus.cpp" line="135"/> + <location filename="settingsdialognexus.cpp" line="140"/> <source>Linked with Nexus successfully.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="215"/> - <source>Failed to set API key</source> + <location filename="settingsdialognexus.cpp" line="137"/> + <source>Failed to store OAuth tokens.</source> <translation type="unfinished"></translation> </message> </context> <context> <name>NexusInterface</name> <message> - <location filename="nexusinterface.cpp" line="357"/> + <location filename="nexusinterface.cpp" line="360"/> <source>Please pick the mod ID for "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="848"/> + <location filename="nexusinterface.cpp" line="854"/> <source>You must authorize MO2 in Settings -> Nexus to use the Nexus API.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="857"/> + <location filename="nexusinterface.cpp" line="863"/> <source>You've exceeded the Nexus API rate limit and requests are now being throttled. Your next batch of requests will be available in approximately %1 minutes and %2 seconds.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="938"/> + <location filename="nexusinterface.cpp" line="944"/> <source>Aborting download: Either you clicked on a premium-only link and your account is not premium, or the download link was generated by a different account than the one stored in Mod Organizer.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="1079"/> + <location filename="nexusinterface.cpp" line="1099"/> <source>empty response</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nexusinterface.cpp" line="1159"/> + <location filename="nexusinterface.cpp" line="1179"/> <source>invalid response</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>NexusManualKeyDialog</name> - <message> - <location filename="nexusmanualkey.ui" line="14"/> - <source>Manual Nexus API Key</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="50"/> - <source>1. Get your personal API key</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="57"/> - <source>Open Browser</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="97"/> - <source>2. Enter your personal API key</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="117"/> - <source>Paste</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="124"/> - <source>Clear</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="146"/> - <source>Enter API key here</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="nexusmanualkey.ui" line="171"/> - <source><b>Sharing this key with anyone could get your Nexus account banned. You can always revoke this key from your account on the Nexus website.</b></source> - <translation type="unfinished"></translation> - </message> -</context> -<context> <name>NexusTab</name> <message> <location filename="modinfodialognexus.cpp" line="156"/> @@ -5996,48 +6009,93 @@ Continue?</source> <context> <name>PluginContainer</name> <message> - <location filename="plugincontainer.cpp" line="1106"/> + <location filename="plugincontainer.cpp" line="89"/> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="94"/> + <source>Diagnose</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="99"/> + <source>Game</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="104"/> + <source>Installer</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="109"/> + <source>Mod Page</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="114"/> + <source>Preview</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="119"/> + <source>Tool</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="124"/> + <source>Proxy</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="129"/> + <source>File Mapper</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="plugincontainer.cpp" line="1104"/> <source>Plugin error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1107"/> + <location filename="plugincontainer.cpp" line="1105"/> <source>Mod Organizer failed to load the plugin '%1' last time it was started.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1110"/> + <location filename="plugincontainer.cpp" line="1108"/> <source>The plugin can be skipped for this session, blacklisted, or loaded normally, in which case it might fail again. Blacklisted plugins can be re-enabled later in the settings.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1115"/> + <location filename="plugincontainer.cpp" line="1113"/> <source>Skip this plugin</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1116"/> + <location filename="plugincontainer.cpp" line="1114"/> <source>Blacklist this plugin</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1117"/> + <location filename="plugincontainer.cpp" line="1115"/> <source>Load this plugin</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1215"/> + <location filename="plugincontainer.cpp" line="1213"/> <source>Some plugins could not be loaded</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1218"/> - <location filename="plugincontainer.cpp" line="1238"/> + <location filename="plugincontainer.cpp" line="1216"/> + <location filename="plugincontainer.cpp" line="1236"/> <source>Description missing</source> <translation type="unfinished"></translation> </message> <message> - <location filename="plugincontainer.cpp" line="1228"/> + <location filename="plugincontainer.cpp" line="1226"/> <source>The following plugins could not be loaded. The reason may be missing dependencies (i.e. python) or an outdated version:</source> <translation type="unfinished"></translation> </message> @@ -6398,49 +6456,6 @@ Continue?</source> </message> </context> <context> - <name>PluginTypeName</name> - <message> - <location filename="plugincontainer.cpp" line="94"/> - <source>Diagnose</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="99"/> - <source>Game</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="104"/> - <source>Installer</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="109"/> - <source>Mod Page</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="114"/> - <source>Preview</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="119"/> - <source>Tool</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="124"/> - <source>Proxy</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="plugincontainer.cpp" line="129"/> - <source>File Mapper</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> <name>PreviewDialog</name> <message> <location filename="previewdialog.ui" line="14"/> @@ -6998,7 +7013,7 @@ p, li { white-space: pre-wrap; } </message> <message> <location filename="createinstancedialogpages.cpp" line="37"/> - <location filename="createinstancedialogpages.cpp" line="1236"/> + <location filename="createinstancedialogpages.cpp" line="1237"/> <source>Instance type: %1</source> <translation type="unfinished"></translation> </message> @@ -7008,193 +7023,192 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="300"/> + <location filename="createinstancedialogpages.cpp" line="301"/> <source>Find game installation for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="348"/> + <location filename="createinstancedialogpages.cpp" line="349"/> <source>Find game installation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="397"/> - <location filename="createinstancedialogpages.cpp" line="686"/> + <location filename="createinstancedialogpages.cpp" line="398"/> <location filename="createinstancedialogpages.cpp" line="687"/> + <location filename="createinstancedialogpages.cpp" line="688"/> <source>Unrecognized game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="399"/> + <location filename="createinstancedialogpages.cpp" line="400"/> <source>The folder %1 does not seem to contain a game Mod Organizer can manage.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="402"/> + <location filename="createinstancedialogpages.cpp" line="403"/> <source>See details for the list of supported games.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="479"/> + <location filename="createinstancedialogpages.cpp" line="480"/> <source>No installation found</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="550"/> + <location filename="createinstancedialogpages.cpp" line="551"/> <source>Browse...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="551"/> + <location filename="createinstancedialogpages.cpp" line="552"/> <source>The folder must contain a valid game installation</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="663"/> <location filename="createinstancedialogpages.cpp" line="664"/> + <location filename="createinstancedialogpages.cpp" line="665"/> <source>Microsoft Store game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="666"/> + <location filename="createinstancedialogpages.cpp" line="667"/> <source>The folder %1 seems to be a Microsoft Store game install. Games installed through the Microsoft Store are not supported by Mod Organizer and will not work properly.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="673"/> - <location filename="createinstancedialogpages.cpp" line="695"/> - <location filename="createinstancedialogpages.cpp" line="721"/> + <location filename="createinstancedialogpages.cpp" line="674"/> + <location filename="createinstancedialogpages.cpp" line="696"/> + <location filename="createinstancedialogpages.cpp" line="722"/> <source>Use this folder for %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="674"/> + <location filename="createinstancedialogpages.cpp" line="675"/> <source>Use this folder</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="675"/> - <location filename="createinstancedialogpages.cpp" line="696"/> - <location filename="createinstancedialogpages.cpp" line="723"/> + <location filename="createinstancedialogpages.cpp" line="676"/> + <location filename="createinstancedialogpages.cpp" line="697"/> + <location filename="createinstancedialogpages.cpp" line="724"/> <source>I know what I'm doing</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="676"/> - <location filename="createinstancedialogpages.cpp" line="697"/> - <location filename="createinstancedialogpages.cpp" line="724"/> - <location filename="settingsdialognexus.cpp" line="270"/> - <location filename="settingsdialognexus.cpp" line="277"/> + <location filename="createinstancedialogpages.cpp" line="677"/> + <location filename="createinstancedialogpages.cpp" line="698"/> + <location filename="createinstancedialogpages.cpp" line="725"/> + <location filename="settingsdialognexus.cpp" line="199"/> <location filename="settingsdialogworkarounds.cpp" line="231"/> - <location filename="spawn.cpp" line="179"/> - <location filename="spawn.cpp" line="203"/> - <location filename="spawn.cpp" line="278"/> - <location filename="spawn.cpp" line="301"/> - <location filename="spawn.cpp" line="328"/> - <location filename="spawn.cpp" line="352"/> - <location filename="spawn.cpp" line="383"/> + <location filename="spawn.cpp" line="181"/> + <location filename="spawn.cpp" line="205"/> + <location filename="spawn.cpp" line="280"/> + <location filename="spawn.cpp" line="303"/> + <location filename="spawn.cpp" line="330"/> + <location filename="spawn.cpp" line="354"/> + <location filename="spawn.cpp" line="385"/> <location filename="uilocker.cpp" line="349"/> <source>Cancel</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="689"/> + <location filename="createinstancedialogpages.cpp" line="690"/> <source>The folder %1 does not seem to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span> or for any other game Mod Organizer can manage.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="708"/> <location filename="createinstancedialogpages.cpp" line="709"/> + <location filename="createinstancedialogpages.cpp" line="710"/> <source>Incorrect game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="711"/> + <location filename="createinstancedialogpages.cpp" line="712"/> <source>The folder %1 seems to contain an installation for <span style="white-space: nowrap; font-weight: bold;">%2</span>, not <span style="white-space: nowrap; font-weight: bold;">%3</span>.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="719"/> + <location filename="createinstancedialogpages.cpp" line="720"/> <source>Manage %1 instead</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1238"/> + <location filename="createinstancedialogpages.cpp" line="1239"/> <source>Instance location: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1241"/> + <location filename="createinstancedialogpages.cpp" line="1242"/> <source>Instance name: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1244"/> + <location filename="createinstancedialogpages.cpp" line="1245"/> <source>Profile settings:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1246"/> + <location filename="createinstancedialogpages.cpp" line="1247"/> <source> Local INIs: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1247"/> - <location filename="createinstancedialogpages.cpp" line="1250"/> - <location filename="createinstancedialogpages.cpp" line="1252"/> + <location filename="createinstancedialogpages.cpp" line="1248"/> + <location filename="createinstancedialogpages.cpp" line="1251"/> + <location filename="createinstancedialogpages.cpp" line="1253"/> <source>yes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1247"/> - <location filename="createinstancedialogpages.cpp" line="1250"/> - <location filename="createinstancedialogpages.cpp" line="1253"/> + <location filename="createinstancedialogpages.cpp" line="1248"/> + <location filename="createinstancedialogpages.cpp" line="1251"/> + <location filename="createinstancedialogpages.cpp" line="1254"/> <source>no</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1249"/> + <location filename="createinstancedialogpages.cpp" line="1250"/> <source> Local Saves: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1251"/> + <location filename="createinstancedialogpages.cpp" line="1252"/> <source> Automatic Archive Invalidation: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1258"/> - <location filename="createinstancedialogpages.cpp" line="1262"/> + <location filename="createinstancedialogpages.cpp" line="1259"/> + <location filename="createinstancedialogpages.cpp" line="1263"/> <source>Base directory: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1263"/> + <location filename="createinstancedialogpages.cpp" line="1264"/> <source>Downloads</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1264"/> + <location filename="createinstancedialogpages.cpp" line="1265"/> <source>Mods</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1265"/> + <location filename="createinstancedialogpages.cpp" line="1266"/> <source>Profiles</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1266"/> + <location filename="createinstancedialogpages.cpp" line="1267"/> <source>Overwrite</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1275"/> + <location filename="createinstancedialogpages.cpp" line="1276"/> <source>Game: %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="createinstancedialogpages.cpp" line="1276"/> + <location filename="createinstancedialogpages.cpp" line="1277"/> <source>Game location: %1</source> <translation type="unfinished"></translation> </message> @@ -7300,7 +7314,7 @@ Destination:<byte value="xd"/> </message> <message> <location filename="installationmanager.cpp" line="73"/> - <source>invalid 7z.so: %1</source> + <source>invalid 7-zip32.dll: %1</source> <translation type="unfinished"></translation> </message> <message> @@ -7438,12 +7452,12 @@ Destination:<byte value="xd"/> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="63"/> + <location filename="main.cpp" line="62"/> <source>Mod Organizer</source> <translation type="unfinished"></translation> </message> <message> - <location filename="main.cpp" line="64"/> + <location filename="main.cpp" line="63"/> <source>An instance of Mod Organizer is already running</source> <translation type="unfinished"></translation> </message> @@ -7515,94 +7529,114 @@ Destination:<byte value="xd"/> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="192"/> + <location filename="nexusoauthlogin.cpp" line="53"/> <source>Connecting to Nexus...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="195"/> + <location filename="nexusoauthlogin.cpp" line="60"/> <source>Waiting for Nexus...</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="198"/> + <location filename="nexusoauthlogin.cpp" line="56"/> <source>Opened Nexus in browser.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="199"/> + <location filename="nexusoauthlogin.cpp" line="57"/> <source>Switch to your browser and accept the request.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="202"/> + <location filename="nexusoauthlogin.cpp" line="63"/> <source>Finished.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="205"/> - <source>No answer from Nexus.</source> + <location filename="nexusoauthlogin.cpp" line="70"/> + <source>An unknown error has occurred.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusoauthlogin.cpp" line="82"/> + <source>No OAuth client id configured.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusoauthlogin.cpp" line="120"/> + <source><html><body><h2>Mod Organizer</h2><p>Authorization complete. You may close this window.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="206"/> - <location filename="nxmaccessmanager.cpp" line="210"/> - <source>A firewall might be blocking Mod Organizer.</source> + <location filename="nexusoauthlogin.cpp" line="125"/> + <source>Failed to bind to localhost on port %1.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="209"/> - <source>Nexus closed the connection.</source> + <location filename="nexusoauthlogin.cpp" line="157"/> + <source>Authorization failed (%1)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="213"/> + <location filename="nexusoauthlogin.cpp" line="198"/> + <source>Internal error: OAuth flow is missing.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusoauthlogin.cpp" line="212"/> + <source>Invalid OAuth token payload.</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="nexusoauthlogin.cpp" line="66"/> <source>Cancelled.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="401"/> + <location filename="nxmaccessmanager.cpp" line="205"/> <source>Failed to request %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="423"/> - <location filename="nxmaccessmanager.cpp" line="753"/> + <location filename="nxmaccessmanager.cpp" line="227"/> + <location filename="nxmaccessmanager.cpp" line="561"/> <source>Cancelled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="469"/> + <location filename="nxmaccessmanager.cpp" line="273"/> <source>Internal error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="504"/> + <location filename="nxmaccessmanager.cpp" line="308"/> <source>HTTP code %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="514"/> + <location filename="nxmaccessmanager.cpp" line="318"/> <source>Invalid JSON</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="519"/> + <location filename="nxmaccessmanager.cpp" line="323"/> <source>Bad response</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="529"/> - <source>API key is empty</source> + <location filename="nxmaccessmanager.cpp" line="188"/> + <location filename="nxmaccessmanager.cpp" line="332"/> + <source>Access token is empty</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="552"/> + <location filename="nxmaccessmanager.cpp" line="355"/> <source>SSL error</source> <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="557"/> + <location filename="nxmaccessmanager.cpp" line="360"/> <source>Timed out</source> <translation type="unfinished"></translation> </message> @@ -7675,19 +7709,19 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="1435"/> - <location filename="settings.cpp" line="1459"/> - <location filename="settings.cpp" line="1507"/> + <location filename="settings.cpp" line="1437"/> + <location filename="settings.cpp" line="1461"/> + <location filename="settings.cpp" line="1509"/> <source>attempt to store setting for unknown plugin "%1"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="2005"/> + <location filename="settings.cpp" line="2007"/> <source>Failed</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settings.cpp" line="2006"/> + <location filename="settings.cpp" line="2008"/> <source>Failed to start the helper application: %1</source> <translation type="unfinished"></translation> </message> @@ -7734,40 +7768,33 @@ This program is known to cause issues with Mod Organizer, such as freezing or bl <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="272"/> - <location filename="settingsdialognexus.cpp" line="282"/> - <location filename="settingsdialognexus.cpp" line="287"/> - <source>Enter API Key Manually</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="settingsdialognexus.cpp" line="275"/> - <location filename="settingsdialognexus.cpp" line="280"/> - <location filename="settingsdialognexus.cpp" line="285"/> + <location filename="settingsdialognexus.cpp" line="202"/> + <location filename="settingsdialognexus.cpp" line="205"/> + <location filename="settingsdialognexus.cpp" line="208"/> <source>Connect to Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="442"/> - <location filename="settingsdialognexus.cpp" line="443"/> - <location filename="settingsdialognexus.cpp" line="444"/> - <location filename="settingsdialognexus.cpp" line="445"/> - <location filename="settingsdialognexus.cpp" line="446"/> + <location filename="settingsdialognexus.cpp" line="363"/> + <location filename="settingsdialognexus.cpp" line="364"/> + <location filename="settingsdialognexus.cpp" line="365"/> + <location filename="settingsdialognexus.cpp" line="366"/> + <location filename="settingsdialognexus.cpp" line="367"/> <source>N/A</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="458"/> + <location filename="settingsdialognexus.cpp" line="379"/> <source>Executables (*.exe)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="458"/> + <location filename="settingsdialognexus.cpp" line="379"/> <source>All Files (*.*)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialognexus.cpp" line="461"/> + <location filename="settingsdialognexus.cpp" line="382"/> <source>Select the browser executable</source> <translation type="unfinished"></translation> </message> @@ -7924,196 +7951,196 @@ Example: <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="137"/> + <location filename="spawn.cpp" line="139"/> <source>This error typically happens because an antivirus has deleted critical files from Mod Organizer's installation folder or has made them generally inaccessible. Add an exclusion for Mod Organizer's installation folder in your antivirus, reinstall Mod Organizer and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="144"/> + <location filename="spawn.cpp" line="146"/> <source>This error typically happens because an antivirus is preventing Mod Organizer from starting programs. Add an exclusion for Mod Organizer's installation folder in your antivirus and try again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="149"/> + <location filename="spawn.cpp" line="151"/> <source>The file '%1' does not exist.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="153"/> + <location filename="spawn.cpp" line="155"/> <source>The working directory '%1' does not exist.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="170"/> - <location filename="spawn.cpp" line="171"/> - <location filename="spawn.cpp" line="196"/> - <location filename="spawn.cpp" line="197"/> + <location filename="spawn.cpp" line="172"/> + <location filename="spawn.cpp" line="173"/> + <location filename="spawn.cpp" line="198"/> + <location filename="spawn.cpp" line="199"/> <source>Cannot start Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="173"/> + <location filename="spawn.cpp" line="175"/> <source>The path to the Steam executable cannot be found. You might try reinstalling Steam.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="177"/> - <location filename="spawn.cpp" line="201"/> - <location filename="spawn.cpp" line="299"/> + <location filename="spawn.cpp" line="179"/> + <location filename="spawn.cpp" line="203"/> + <location filename="spawn.cpp" line="301"/> <source>Continue without starting Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="178"/> - <location filename="spawn.cpp" line="202"/> + <location filename="spawn.cpp" line="180"/> + <location filename="spawn.cpp" line="204"/> <source>The program may fail to launch.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="212"/> + <location filename="spawn.cpp" line="214"/> <source>Cannot launch program</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="214"/> - <location filename="spawn.cpp" line="238"/> - <location filename="spawn.cpp" line="256"/> + <location filename="spawn.cpp" line="216"/> + <location filename="spawn.cpp" line="240"/> + <location filename="spawn.cpp" line="258"/> <source>Cannot start %1</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="236"/> + <location filename="spawn.cpp" line="238"/> <source>Cannot launch helper</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="254"/> - <location filename="spawn.cpp" line="309"/> + <location filename="spawn.cpp" line="256"/> + <location filename="spawn.cpp" line="311"/> <source>Elevation required</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="258"/> + <location filename="spawn.cpp" line="260"/> <source>This program is requesting to run as administrator but Mod Organizer itself is not running as administrator. Running programs as administrator is typically unnecessary as long as the game and Mod Organizer have been installed outside "Program Files".<byte value="xd"/> <byte value="xd"/> You can restart Mod Organizer as administrator and try launching the program again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="274"/> - <location filename="spawn.cpp" line="323"/> + <location filename="spawn.cpp" line="276"/> + <location filename="spawn.cpp" line="325"/> <source>Restart Mod Organizer as administrator</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="275"/> - <location filename="spawn.cpp" line="324"/> + <location filename="spawn.cpp" line="277"/> + <location filename="spawn.cpp" line="326"/> <source>You must allow "helper.exe" to make changes to the system.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="287"/> + <location filename="spawn.cpp" line="289"/> <source>Launch Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="288"/> + <location filename="spawn.cpp" line="290"/> <source>This program requires Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="289"/> + <location filename="spawn.cpp" line="291"/> <source>Mod Organizer has detected that this program likely requires Steam to be running to function properly.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="298"/> + <location filename="spawn.cpp" line="300"/> <source>Start Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="300"/> - <location filename="spawn.cpp" line="326"/> + <location filename="spawn.cpp" line="302"/> + <location filename="spawn.cpp" line="328"/> <source>The program might fail to run.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="310"/> + <location filename="spawn.cpp" line="312"/> <source>Steam is running as administrator</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="311"/> + <location filename="spawn.cpp" line="313"/> <source>Running Steam as administrator is typically unnecessary and can cause problems when Mod Organizer itself is not running as administrator.<byte value="xd"/> <byte value="xd"/> You can restart Mod Organizer as administrator and try launching the program again.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="326"/> - <location filename="spawn.cpp" line="350"/> - <location filename="spawn.cpp" line="380"/> + <location filename="spawn.cpp" line="328"/> + <location filename="spawn.cpp" line="352"/> + <location filename="spawn.cpp" line="382"/> <source>Continue</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="336"/> + <location filename="spawn.cpp" line="338"/> <source>Event Log not running</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="337"/> + <location filename="spawn.cpp" line="339"/> <source>The Event Log service is not running</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="338"/> + <location filename="spawn.cpp" line="340"/> <source>The Windows Event Log service is not running. This can prevent USVFS from running properly and your mods may not be recognized by the program being launched.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="350"/> - <location filename="spawn.cpp" line="381"/> + <location filename="spawn.cpp" line="352"/> + <location filename="spawn.cpp" line="383"/> <source>Your mods might not work.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="361"/> + <location filename="spawn.cpp" line="363"/> <source>Blacklisted program</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="363"/> + <location filename="spawn.cpp" line="365"/> <source>The program %1 is blacklisted</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="364"/> + <location filename="spawn.cpp" line="366"/> <source>The program you are attempting to launch is blacklisted in the virtual filesystem. This will likely prevent it from seeing any mods, INI files or any other virtualized files.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="382"/> + <location filename="spawn.cpp" line="384"/> <source>Change the blacklist</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="603"/> + <location filename="spawn.cpp" line="605"/> <source>Waiting</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="604"/> + <location filename="spawn.cpp" line="606"/> <source>Please press OK once you're logged into steam.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="892"/> + <location filename="spawn.cpp" line="894"/> <source>Select binary</source> <translation type="unfinished"></translation> </message> <message> - <location filename="spawn.cpp" line="893"/> + <location filename="spawn.cpp" line="895"/> <source>Binary</source> <translation type="unfinished"></translation> </message> @@ -8730,7 +8757,7 @@ If you disable this feature, MO will only display official DLCs this way. Please <location filename="settingsdialog.ui" line="847"/> <location filename="settingsdialog.ui" line="944"/> <location filename="settingsdialog.ui" line="1002"/> - <location filename="settingsdialog.ui" line="1981"/> + <location filename="settingsdialog.ui" line="1971"/> <source>...</source> <translation type="unfinished"></translation> </message> @@ -8847,205 +8874,195 @@ If you disable this feature, MO will only display official DLCs this way. Please </message> <message> <location filename="settingsdialog.ui" line="1207"/> - <source>Manually enter the API key and try to login</source> + <source>Clear the stored Nexus authorization and force reauthorization.</source> <translation type="unfinished"></translation> </message> <message> <location filename="settingsdialog.ui" line="1210"/> - <source>Enter API Key Manually</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="settingsdialog.ui" line="1217"/> - <source>Clear the stored Nexus API key and force reauthorization.</source> - <translation type="unfinished"></translation> - </message> - <message> - <location filename="settingsdialog.ui" line="1220"/> <source>Disconnect from Nexus</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1293"/> - <location filename="settingsdialog.ui" line="1768"/> + <location filename="settingsdialog.ui" line="1283"/> + <location filename="settingsdialog.ui" line="1758"/> <source>Options</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1326"/> + <location filename="settingsdialog.ui" line="1316"/> <source>Endorsement Integration</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1336"/> + <location filename="settingsdialog.ui" line="1326"/> <source>Tracked Integration</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1346"/> + <location filename="settingsdialog.ui" line="1336"/> <source>Use Nexus category mappings</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1356"/> - <location filename="settingsdialog.ui" line="1359"/> + <location filename="settingsdialog.ui" line="1346"/> + <location filename="settingsdialog.ui" line="1349"/> <source><html><head/><body><p>By default, a counter is displayed in the bottom right corner. This informs the user of their remaining API requests. The Nexus API becomes unusable once these API requests run out. Checking this option will hide that counter.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1362"/> + <location filename="settingsdialog.ui" line="1352"/> <source>Hide API Request Counter</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1387"/> + <location filename="settingsdialog.ui" line="1377"/> <source>Associate with "Download with manager" links</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1394"/> + <location filename="settingsdialog.ui" line="1384"/> <source>Remove cache and cookies.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1397"/> + <location filename="settingsdialog.ui" line="1387"/> <source>Clear Cache</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1410"/> + <location filename="settingsdialog.ui" line="1400"/> <source>Servers</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1433"/> + <location filename="settingsdialog.ui" line="1423"/> <source>Known Servers (updated on download)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1454"/> + <location filename="settingsdialog.ui" line="1444"/> <source>Preferred Servers (Drag & Drop)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1481"/> + <location filename="settingsdialog.ui" line="1471"/> <source>Plugins</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1582"/> + <location filename="settingsdialog.ui" line="1572"/> <source>Author:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1602"/> + <location filename="settingsdialog.ui" line="1592"/> <source>Version:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1622"/> + <location filename="settingsdialog.ui" line="1612"/> <source>Description:</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1645"/> + <location filename="settingsdialog.ui" line="1635"/> <source>Enabled</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1674"/> + <location filename="settingsdialog.ui" line="1664"/> <source>Key</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1679"/> + <location filename="settingsdialog.ui" line="1669"/> <source>Value</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1687"/> + <location filename="settingsdialog.ui" line="1677"/> <source>No plugin found.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1708"/> + <location filename="settingsdialog.ui" line="1698"/> <source>Blacklisted Plugins (use <del> to remove):</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1722"/> + <location filename="settingsdialog.ui" line="1712"/> <source>Workarounds</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1786"/> + <location filename="settingsdialog.ui" line="1776"/> <source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1789"/> + <location filename="settingsdialog.ui" line="1779"/> <source>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1793"/> + <location filename="settingsdialog.ui" line="1783"/> <source>Force-enable game files</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1803"/> + <location filename="settingsdialog.ui" line="1793"/> <source>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1806"/> + <location filename="settingsdialog.ui" line="1796"/> <source><html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html></source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1809"/> + <location filename="settingsdialog.ui" line="1799"/> <source>Enable archives parsing (experimental)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1819"/> - <location filename="settingsdialog.ui" line="1822"/> + <location filename="settingsdialog.ui" line="1809"/> + <location filename="settingsdialog.ui" line="1812"/> <source>Disable this to prevent the GUI from being locked when running an executable. This may result in abnormal behavior.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1825"/> + <location filename="settingsdialog.ui" line="1815"/> <source>Lock GUI when running executable</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1838"/> + <location filename="settingsdialog.ui" line="1828"/> <source>Steam</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1847"/> + <location filename="settingsdialog.ui" line="1837"/> <source>Password</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1861"/> + <location filename="settingsdialog.ui" line="1851"/> <source>Username</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1868"/> + <location filename="settingsdialog.ui" line="1858"/> <source>Steam App ID</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1875"/> + <location filename="settingsdialog.ui" line="1865"/> <source>The Steam AppID for your game</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1878"/> + <location filename="settingsdialog.ui" line="1868"/> <source><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -9061,69 +9078,69 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1905"/> + <location filename="settingsdialog.ui" line="1895"/> <source>Network</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1911"/> + <location filename="settingsdialog.ui" line="1901"/> <source>Disable automatic internet features</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1914"/> + <location filename="settingsdialog.ui" line="1904"/> <source>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1917"/> + <location filename="settingsdialog.ui" line="1907"/> <source>Offline Mode</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1924"/> + <location filename="settingsdialog.ui" line="1914"/> <source>Use a proxy for network connections.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1927"/> + <location filename="settingsdialog.ui" line="1917"/> <source>Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1930"/> + <location filename="settingsdialog.ui" line="1920"/> <source>Use System HTTP Proxy</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1952"/> - <location filename="settingsdialog.ui" line="1955"/> + <location filename="settingsdialog.ui" line="1942"/> + <location filename="settingsdialog.ui" line="1945"/> + <location filename="settingsdialog.ui" line="1948"/> <location filename="settingsdialog.ui" line="1958"/> - <location filename="settingsdialog.ui" line="1968"/> - <location filename="settingsdialog.ui" line="1971"/> - <location filename="settingsdialog.ui" line="1974"/> + <location filename="settingsdialog.ui" line="1961"/> + <location filename="settingsdialog.ui" line="1964"/> <source>Use "%1" as a placeholder for the URL.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="1961"/> + <location filename="settingsdialog.ui" line="1951"/> <source>Custom browser</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2009"/> - <location filename="settingsdialog.ui" line="2012"/> + <location filename="settingsdialog.ui" line="1999"/> + <location filename="settingsdialog.ui" line="2002"/> <source>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2015"/> + <location filename="settingsdialog.ui" line="2005"/> <source>Reset Window Geometries</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2022"/> - <location filename="settingsdialog.ui" line="2028"/> + <location filename="settingsdialog.ui" line="2012"/> + <location filename="settingsdialog.ui" line="2018"/> <source> For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! @@ -9131,12 +9148,12 @@ p, li { white-space: pre-wrap; } <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2034"/> + <location filename="settingsdialog.ui" line="2024"/> <source>Back-date BSAs</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2076"/> + <location filename="settingsdialog.ui" line="2066"/> <source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended @@ -9145,64 +9162,64 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2083"/> + <location filename="settingsdialog.ui" line="2073"/> <source>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2086"/> + <location filename="settingsdialog.ui" line="2076"/> <source>Executables Blacklist</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2096"/> - <location filename="settingsdialog.ui" line="2099"/> + <location filename="settingsdialog.ui" line="2086"/> + <location filename="settingsdialog.ui" line="2089"/> <source>Files to skip or ignore from the virtual file system.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2102"/> + <location filename="settingsdialog.ui" line="2092"/> <source>Skip File Suffixes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2112"/> - <location filename="settingsdialog.ui" line="2115"/> + <location filename="settingsdialog.ui" line="2102"/> + <location filename="settingsdialog.ui" line="2105"/> <source>Directories to skip or ignore from the virtual file system.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2118"/> + <location filename="settingsdialog.ui" line="2108"/> <source>Skip Directories</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2161"/> + <location filename="settingsdialog.ui" line="2151"/> <source>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2172"/> + <location filename="settingsdialog.ui" line="2162"/> <source>Diagnostics</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2181"/> + <location filename="settingsdialog.ui" line="2171"/> <source>Logs and Crashes</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2190"/> + <location filename="settingsdialog.ui" line="2180"/> <source>Log Level</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2197"/> + <location filename="settingsdialog.ui" line="2187"/> <source>Decides the amount of data printed to "ModOrganizer.log"</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2200"/> + <location filename="settingsdialog.ui" line="2190"/> <source> Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regular use. On the "Error" level the log file usually remains empty. @@ -9210,17 +9227,17 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2210"/> + <location filename="settingsdialog.ui" line="2200"/> <source>Crash Dumps</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2217"/> + <location filename="settingsdialog.ui" line="2207"/> <source>Decides which type of crash dumps are collected when injected processes crash.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2220"/> + <location filename="settingsdialog.ui" line="2210"/> <source> Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -9231,17 +9248,17 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2233"/> + <location filename="settingsdialog.ui" line="2223"/> <source>Max Dumps To Keep</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2240"/> + <location filename="settingsdialog.ui" line="2230"/> <source>Maximum number of crash dumps to keep on disk. Use 0 for unlimited.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2243"/> + <location filename="settingsdialog.ui" line="2233"/> <source> Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -9249,22 +9266,22 @@ programs you are intentionally running.</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2256"/> + <location filename="settingsdialog.ui" line="2246"/> <source>Integrated LOOT</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2265"/> + <location filename="settingsdialog.ui" line="2255"/> <source>LOOT Log Level</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2278"/> + <location filename="settingsdialog.ui" line="2268"/> <source>Click a link to open the location</source> <translation type="unfinished"></translation> </message> <message> - <location filename="settingsdialog.ui" line="2281"/> + <location filename="settingsdialog.ui" line="2271"/> <source> Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -9326,14 +9343,6 @@ programs you are intentionally running.</source> </message> </context> <context> - <name>T</name> - <message> - <location filename="plugincontainer.cpp" line="89"/> - <source>Plugin</source> - <translation type="unfinished"></translation> - </message> -</context> -<context> <name>TransferSavesDialog</name> <message> <location filename="transfersavesdialog.ui" line="14"/> @@ -9479,7 +9488,7 @@ On Windows XP: </message> <message> <location filename="validationprogressdialog.ui" line="35"/> - <location filename="nxmaccessmanager.cpp" line="150"/> + <location filename="nxmaccessmanager.cpp" line="151"/> <source>Connecting to Nexus...</source> <translation type="unfinished"></translation> </message> @@ -9494,7 +9503,7 @@ On Windows XP: <translation type="unfinished"></translation> </message> <message> - <location filename="nxmaccessmanager.cpp" line="148"/> + <location filename="nxmaccessmanager.cpp" line="149"/> <source>Trying again...</source> <translation type="unfinished"></translation> </message> @@ -9840,7 +9849,8 @@ Please open the "Nexus" tab.</source> </message> <message> <location filename="tutorials/tutorial_firststeps_settings.js" line="21"/> - <source>Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile.</source> + <source>Use this interface to authorize Mod Organizer with Nexus Mods. This login is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store these credentials securely.</source> + <oldsource>Use this interface to authorize Mod Organizer with Nexus Mods. This login is used for all Nexus API connections such as downloads and update checks. MO2 uses the Windows Credential Manager to store the resulting OAuth tokens securely.</oldsource> <translation type="unfinished"></translation> </message> </context> diff --git a/src/src/organizercore.cpp b/src/src/organizercore.cpp index d42b1ac..c469033 100644 --- a/src/src/organizercore.cpp +++ b/src/src/organizercore.cpp @@ -369,6 +369,14 @@ void OrganizerCore::updateModInfoFromDisc() m_Settings.refreshThreadCount()); } +void OrganizerCore::showNotification(const QString& title, const QString& message, + QSystemTrayIcon::MessageIcon icon) +{ + if (m_UserInterface) { + m_UserInterface->showNotification(title, message, icon); + } +} + void OrganizerCore::setUserInterface(IUserInterface* ui) { storeSettings(); diff --git a/src/src/organizercore.h b/src/src/organizercore.h index 7ef7713..494e835 100644 --- a/src/src/organizercore.h +++ b/src/src/organizercore.h @@ -8,6 +8,7 @@ #include <QSettings>
#include <QString>
#include <QStringList>
+#include <QSystemTrayIcon>
#include <QThread>
#include <QVariant>
@@ -372,6 +373,10 @@ public: static void setGlobalCoreDumpType(env::CoreDumpTypes type);
static std::wstring getGlobalCoreDumpPath();
+ void showNotification(
+ const QString& title, const QString& message,
+ QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon::Information);
+
public:
MOBase::IModRepositoryBridge* createNexusBridge() const;
QString profileName() const;
diff --git a/src/src/settings.cpp b/src/src/settings.cpp index c665756..6cea57f 100644 --- a/src/src/settings.cpp +++ b/src/src/settings.cpp @@ -335,64 +335,32 @@ void Settings::setFirstStart(bool b) QString Settings::executablesBlacklist() const { - static const QString def = (QStringList() << "Chrome.exe" - << "Firefox.exe" - << "TSVNCache.exe" - << "TGitCache.exe" - << "Steam.exe" - << "GameOverlayUI.exe" - << "Discord.exe" - << "GalaxyClient.exe" - << "Spotify.exe" - << "Brave.exe") - .join(";"); - - return get<QString>(m_Settings, "Settings", "executable_blacklist", def); + // USVFS-era setting; FUSE VFS on Linux doesn't hook executables per-process. + return ""; } -bool Settings::isExecutableBlacklisted(const QString& s) const +bool Settings::isExecutableBlacklisted(const QString&) const { - for (auto exec : executablesBlacklist().split(";")) { - if (exec.compare(s, Qt::CaseInsensitive) == 0) { - return true; - } - } - return false; } -void Settings::setExecutablesBlacklist(const QString& s) -{ - set(m_Settings, "Settings", "executable_blacklist", s); -} +void Settings::setExecutablesBlacklist(const QString&) {} QStringList Settings::skipFileSuffixes() const { - static const QStringList def = QStringList() << ".mohidden"; - - auto setting = get<QStringList>(m_Settings, "Settings", "skip_file_suffixes", def); - - return setting; + // Keep the historical default so existing modlists with .mohidden files + // keep ignoring them; previously this was a user-editable Workarounds entry. + return {".mohidden"}; } -void Settings::setSkipFileSuffixes(const QStringList& s) -{ - set(m_Settings, "Settings", "skip_file_suffixes", s); -} +void Settings::setSkipFileSuffixes(const QStringList&) {} QStringList Settings::skipDirectories() const { - static const QStringList def = QStringList() << ".git"; - - auto setting = get<QStringList>(m_Settings, "Settings", "skip_directories", def); - - return setting; + return {".git"}; } -void Settings::setSkipDirectories(const QStringList& s) -{ - set(m_Settings, "Settings", "skip_directories", s); -} +void Settings::setSkipDirectories(const QStringList&) {} void Settings::setMotdHash(uint hash) { @@ -671,12 +639,15 @@ void GameSettings::setPlugin(const MOBase::IPluginGame* gamePlugin) bool GameSettings::forceEnableCoreFiles() const { - return get<bool>(m_Settings, "Settings", "force_enable_core_files", true); + // Hardcoded on. Disabling primary-master autoload caused user issue + // where DLCs went unchecked after a tab refresh; there's no use-case + // outside Nehrim-style total conversions that justifies the footgun. + return true; } -void GameSettings::setForceEnableCoreFiles(bool b) +void GameSettings::setForceEnableCoreFiles(bool) { - set(m_Settings, "Settings", "force_enable_core_files", b); + // no-op; see forceEnableCoreFiles(). } std::optional<QString> GameSettings::directory() const @@ -1801,32 +1772,23 @@ NetworkSettings::NetworkSettings(QSettings& settings, bool globalInstance) void NetworkSettings::updateCustomBrowser() const { - if (useCustomBrowser()) { - MOBase::shell::SetUrlHandler(customBrowserCommand()); - } else { - MOBase::shell::SetUrlHandler(""); - } + // Removed alongside the Workarounds tab; system mimetypes drive URL handling. + MOBase::shell::SetUrlHandler(""); } bool NetworkSettings::offlineMode() const { - return get<bool>(m_Settings, "Settings", "offline_mode", false); + return false; } -void NetworkSettings::setOfflineMode(bool b) -{ - set(m_Settings, "Settings", "offline_mode", b); -} +void NetworkSettings::setOfflineMode(bool) {} bool NetworkSettings::useProxy() const { - return get<bool>(m_Settings, "Settings", "use_proxy", false); + return false; } -void NetworkSettings::setUseProxy(bool b) -{ - set(m_Settings, "Settings", "use_proxy", b); -} +void NetworkSettings::setUseProxy(bool) {} void NetworkSettings::setDownloadSpeed(const QString& name, int bytesPerSecond) { @@ -1935,25 +1897,17 @@ void NetworkSettings::updateFromOldMap() bool NetworkSettings::useCustomBrowser() const { - return get<bool>(m_Settings, "Settings", "use_custom_browser", false); + return false; } -void NetworkSettings::setUseCustomBrowser(bool b) -{ - set(m_Settings, "Settings", "use_custom_browser", b); - updateCustomBrowser(); -} +void NetworkSettings::setUseCustomBrowser(bool) {} QString NetworkSettings::customBrowserCommand() const { - return get<QString>(m_Settings, "Settings", "custom_browser", ""); + return ""; } -void NetworkSettings::setCustomBrowserCommand(const QString& s) -{ - set(m_Settings, "Settings", "custom_browser", s); - updateCustomBrowser(); -} +void NetworkSettings::setCustomBrowserCommand(const QString&) {} ServerList NetworkSettings::serversFromOldMap() const { @@ -2133,41 +2087,22 @@ SteamSettings::SteamSettings(Settings& parent, QSettings& settings) QString SteamSettings::appID() const { - return get<QString>(m_Settings, "Settings", "app_id", - m_Parent.game().plugin()->steamAPPId()); + // The user-override `app_id` ini key is no longer exposed; per-game plugins + // already carry the right value via steamAPPId(), and per-executable overrides + // live on individual Executable entries. + return m_Parent.game().plugin()->steamAPPId(); } -void SteamSettings::setAppID(const QString& id) -{ - if (id.isEmpty()) { - remove(m_Settings, "Settings", "app_id"); - } else { - set(m_Settings, "Settings", "app_id", id); - } -} +void SteamSettings::setAppID(const QString&) {} bool SteamSettings::login(QString& username, QString& password) const { - username = get<QString>(m_Settings, "Settings", "steam_username", ""); - password = getWindowsCredential("steam_password"); - - return !username.isEmpty() && !password.isEmpty(); + username.clear(); + password.clear(); + return false; } -void SteamSettings::setLogin(QString username, QString password) -{ - if (username == "") { - remove(m_Settings, "Settings", "steam_username"); - password = ""; - } else { - set(m_Settings, "Settings", "steam_username", username); - } - - if (!setWindowsCredential("steam_password", password)) { - const auto e = GetLastError(); - log::error("Storing or deleting password failed: {}", formatSystemMessage(e)); - } -} +void SteamSettings::setLogin(QString, QString) {} InterfaceSettings::InterfaceSettings(QSettings& settings) : m_Settings(settings) {} @@ -2298,6 +2233,16 @@ void InterfaceSettings::setHideDownloadsAfterInstallation(bool b) set(m_Settings, "Settings", "autohide_downloads", b); } +bool InterfaceSettings::showDownloadNotifications() const +{ + return get<bool>(m_Settings, "Settings", "download_notifications", false); +} + +void InterfaceSettings::setShowDownloadNotifications(bool b) +{ + set(m_Settings, "Settings", "download_notifications", b); +} + bool InterfaceSettings::hideAPICounter() const { return get<bool>(m_Settings, "Settings", "hide_api_counter", false); diff --git a/src/src/settings.h b/src/src/settings.h index 6726b2f..92f71fa 100644 --- a/src/src/settings.h +++ b/src/src/settings.h @@ -647,6 +647,11 @@ public: bool hideDownloadsAfterInstallation() const; void setHideDownloadsAfterInstallation(bool b); + // whether to show notifications when downloads complete or fail + // + bool showDownloadNotifications() const; + void setShowDownloadNotifications(bool b); + // whether the API counter should be hidden // bool hideAPICounter() const; diff --git a/src/src/settingsdialog.cpp b/src/src/settingsdialog.cpp index e07ac71..6b0fca3 100644 --- a/src/src/settingsdialog.cpp +++ b/src/src/settingsdialog.cpp @@ -27,7 +27,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "settingsdialogproton.h"
#include "settingsdialogtheme.h"
#include "settingsdialogupdates.h"
-#include "settingsdialogworkarounds.h"
#include "ui_settingsdialog.h"
using namespace MOBase;
@@ -54,8 +53,6 @@ SettingsDialog::SettingsDialog(PluginContainer* pluginContainer, Settings& setti new PluginsSettingsTab(settings, m_pluginContainer, *this)));
m_tabs.push_back(
std::unique_ptr<SettingsTab>(new ProtonSettingsTab(settings, *this)));
- m_tabs.push_back(
- std::unique_ptr<SettingsTab>(new WorkaroundsSettingsTab(settings, *this)));
}
PluginContainer* SettingsDialog::pluginContainer()
diff --git a/src/src/settingsdialog.ui b/src/src/settingsdialog.ui index 04dcd3e..a7e2a90 100644 --- a/src/src/settingsdialog.ui +++ b/src/src/settingsdialog.ui @@ -166,6 +166,35 @@ </property> </widget> </item> + <item> + <widget class="QCheckBox" name="showDownloadNotificationsBox"> + <property name="toolTip"> + <string>Show notifications when downloads complete or fail.</string> + </property> + <property name="whatsThis"> + <string>Show notifications when downloads complete or fail.</string> + </property> + <property name="text"> + <string>Show notifications for completed or failed downloads</string> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="enableArchiveParsingBox"> + <property name="toolTip"> + <string>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</string> + </property> + <property name="whatsThis"> + <string><html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html></string> + </property> + <property name="text"> + <string>Enable archives parsing (experimental)</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> </layout> </widget> </item> @@ -1897,440 +1926,6 @@ If you disable this feature, MO will only display official DLCs this way. Please </item> </layout> </widget> - <widget class="QWidget" name="workaroundTab"> - <attribute name="title"> - <string>Workarounds</string> - </attribute> - <layout class="QVBoxLayout" name="verticalLayout_5"> - <item> - <widget class="QScrollArea" name="workaroundScrollArea"> - <property name="styleSheet"> - <string notr="true">#workaroundScrollArea { background-color: transparent; } -#workaroundScrollAreaWidgetContents { background-color: transparent; }</string> - </property> - <property name="frameShape"> - <enum>QFrame::NoFrame</enum> - </property> - <property name="frameShadow"> - <enum>QFrame::Plain</enum> - </property> - <property name="widgetResizable"> - <bool>true</bool> - </property> - <widget class="QWidget" name="workaroundScrollAreaWidgetContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>778</width> - <height>475</height> - </rect> - </property> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QGroupBox" name="workaroundsOptionsBox"> - <property name="title"> - <string>Options</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_23"> - <property name="leftMargin"> - <number>9</number> - </property> - <property name="topMargin"> - <number>9</number> - </property> - <property name="rightMargin"> - <number>9</number> - </property> - <property name="bottomMargin"> - <number>9</number> - </property> - <item> - <widget class="QCheckBox" name="forceEnableBox"> - <property name="toolTip"> - <string>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on)</string> - </property> - <property name="whatsThis"> - <string>If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) -Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled.</string> - </property> - <property name="text"> - <string>Force-enable game files</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="enableArchiveParsingBox"> - <property name="toolTip"> - <string>Enable parsing of Archives. This is an Experimental Feature. Has negative effects on performance and known incorrectness.</string> - </property> - <property name="whatsThis"> - <string><html><head/><body><p>By default, MO will parse archive files (BSA, BA2) to calculate conflicts between the contents of the archive files and other loose files. This process has a noticeable cost in performance.</p><p>This feature should not be confused with the archive management feature offered by MO1. MO2 will only show conflicts with archives and will NOT load them into the game or program.</p><p>If you disable this feature, MO will only display conflicts between loose files.</p></body></html></string> - </property> - <property name="text"> - <string>Enable archives parsing (experimental)</string> - </property> - <property name="checked"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox_12"> - <property name="title"> - <string>Steam</string> - </property> - <layout class="QGridLayout" name="gridLayout_7"> - <item row="1" column="1"> - <widget class="QLineEdit" name="steamUserEdit"/> - </item> - <item row="1" column="2"> - <widget class="QLabel" name="label_20"> - <property name="text"> - <string>Password</string> - </property> - </widget> - </item> - <item row="1" column="3"> - <widget class="QLineEdit" name="steamPassEdit"> - <property name="echoMode"> - <enum>QLineEdit::Password</enum> - </property> - </widget> - </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_19"> - <property name="text"> - <string>Username</string> - </property> - </widget> - </item> - <item row="0" column="0"> - <widget class="QLabel" name="label_4"> - <property name="text"> - <string>Steam App ID</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="appIDEdit"> - <property name="toolTip"> - <string>The Steam AppID for your game</string> - </property> - <property name="whatsThis"> - <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">The Steam App ID is required to directly start some games. For Skyrim, if this is not set or wrong, the &quot;Mod Organizer&quot; load mechanism may not work properly.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">The preset for this is the App ID of the &quot;regular&quot; version so in most cases, you should be set.</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you think you have a different version (GotY or something), follow these steps to get to the id:</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">1. Navigate to the game library in steam</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">2. right-click on the game you need the id for and choose </span><span style=" font-size:8pt; font-weight:600;">Create desktop shortcut</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">3. right-click on the newly created shortcut on your desktop and select </span><span style=" font-size:8pt; font-weight:600;">Properties</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">4. in the URL-field you should see something like this: </span><span style=" font-size:8pt; font-style:italic;">steam://rungameid/22380</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">22380 is the id you're looking for.</span></p></body></html></string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QGroupBox" name="groupBox"> - <property name="minimumSize"> - <size> - <width>0</width> - <height>100</height> - </size> - </property> - <property name="title"> - <string>Network</string> - </property> - <layout class="QVBoxLayout" name="verticalLayout_22"> - <item> - <widget class="QCheckBox" name="offlineBox"> - <property name="statusTip"> - <string>Disable automatic internet features</string> - </property> - <property name="whatsThis"> - <string>Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser)</string> - </property> - <property name="text"> - <string>Offline Mode</string> - </property> - </widget> - </item> - <item> - <widget class="QCheckBox" name="proxyBox"> - <property name="statusTip"> - <string>Use a proxy for network connections.</string> - </property> - <property name="whatsThis"> - <string>Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy.</string> - </property> - <property name="text"> - <string>Use System HTTP Proxy</string> - </property> - </widget> - </item> - <item> - <widget class="QWidget" name="widget_7" native="true"> - <layout class="QHBoxLayout" name="horizontalLayout_9"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QCheckBox" name="useCustomBrowser"> - <property name="toolTip"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - <property name="statusTip"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - <property name="whatsThis"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - <property name="text"> - <string>Custom browser</string> - </property> - </widget> - </item> - <item> - <widget class="QLineEdit" name="browserCommand"> - <property name="toolTip"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - <property name="statusTip"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - <property name="whatsThis"> - <string>Use "%1" as a placeholder for the URL.</string> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="browseCustomBrowser"> - <property name="text"> - <string>...</string> - </property> - </widget> - </item> - </layout> - </widget> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QWidget" name="widget_12" native="true"> - <layout class="QHBoxLayout" name="horizontalLayout_usvfs"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QPushButton" name="resetGeometryBtn"> - <property name="toolTip"> - <string>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</string> - </property> - <property name="whatsThis"> - <string>Resets the window geometries for all windows. This can be useful if a window becomes too small or too large, if a column becomes too thin or too wide, and in similar situations.</string> - </property> - <property name="text"> - <string>Reset Window Geometries</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="bsaDateBtn"> - <property name="toolTip"> - <string> - For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. - For the other games this is not a sufficient replacement for AI! - </string> - </property> - <property name="whatsThis"> - <string> - For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. - For the other games this is not a sufficient replacement for AI! - </string> - </property> - <property name="text"> - <string>Back-date BSAs</string> - </property> - <property name="icon"> - <iconset> - <normaloff>:/MO/gui/resources/emblem-readonly.png</normaloff>:/MO/gui/resources/emblem-readonly.png</iconset> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer_usvfs"> - <property name="orientation"> - <enum>Qt::Orientation::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <widget class="QWidget" name="widget_usvfs" native="true"> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>0</number> - </property> - <property name="rightMargin"> - <number>0</number> - </property> - <property name="bottomMargin"> - <number>0</number> - </property> - <item> - <widget class="QPushButton" name="execBlacklistBtn"> - <property name="toolTip"> - <string>Add executables to the blacklist to prevent them from -accessing the virtual file system. This is useful to prevent -unintended programs from being hooked. Hooking unintended -programs may affect the execution of these programs or the -programs you are intentionally running.</string> - </property> - <property name="whatsThis"> - <string>Add executables to the blacklist to prevent them from accessing the virtual file system. This is useful to prevent unintended programs from being hooked. Hooking unintended programs may affect the execution of these programs or the programs you are intentionally running.</string> - </property> - <property name="text"> - <string>Executables Blacklist</string> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="skipFileSuffixBtn"> - <property name="toolTip"> - <string>Files to skip or ignore from the virtual file system.</string> - </property> - <property name="whatsThis"> - <string>Files to skip or ignore from the virtual file system.</string> - </property> - <property name="text"> - <string>Skip File Suffixes</string> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="skipDirectoriesBtn"> - <property name="toolTip"> - <string>Directories to skip or ignore from the virtual file system.</string> - </property> - <property name="whatsThis"> - <string>Directories to skip or ignore from the virtual file system.</string> - </property> - <property name="text"> - <string>Skip Directories</string> - </property> - <property name="autoDefault"> - <bool>false</bool> - </property> - </widget> - </item> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> - </layout> - </widget> - </widget> - </item> - <item> - <widget class="QLabel" name="label_6"> - <property name="text"> - <string>These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here.</string> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - </layout> - </widget> <widget class="QWidget" name="diagnosticsTab"> <attribute name="title"> <string>Diagnostics</string> diff --git a/src/src/settingsdialoggeneral.cpp b/src/src/settingsdialoggeneral.cpp index 81af13c..c40f667 100644 --- a/src/src/settingsdialoggeneral.cpp +++ b/src/src/settingsdialoggeneral.cpp @@ -20,6 +20,9 @@ GeneralSettingsTab::GeneralSettingsTab(Settings& s, SettingsDialog& d) ui->showMetaBox->setChecked(settings().interface().metaDownloads()); ui->hideDownloadInstallBox->setChecked( settings().interface().hideDownloadsAfterInstallation()); + ui->showDownloadNotificationsBox->setChecked( + settings().interface().showDownloadNotifications()); + ui->enableArchiveParsingBox->setChecked(settings().archiveParsing()); // Update settings have moved to their own "Updates" tab. Hide the // legacy controls here so the two UIs don't contradict each other. @@ -65,6 +68,9 @@ void GeneralSettingsTab::update() settings().interface().setMetaDownloads(ui->showMetaBox->isChecked()); settings().interface().setHideDownloadsAfterInstallation( ui->hideDownloadInstallBox->isChecked()); + settings().interface().setShowDownloadNotifications( + ui->showDownloadNotificationsBox->isChecked()); + settings().setArchiveParsing(ui->enableArchiveParsingBox->isChecked()); // Update settings are persisted by UpdatesSettingsTab (own tab). diff --git a/src/src/settingsdialognexus.cpp b/src/src/settingsdialognexus.cpp index fac2a41..d4008be 100644 --- a/src/src/settingsdialognexus.cpp +++ b/src/src/settingsdialognexus.cpp @@ -393,15 +393,7 @@ NexusSettingsTab::NexusSettingsTab(Settings& s, SettingsDialog& d) : SettingsTab QObject::connect(ui->associateButton, &QPushButton::clicked, [&] { associate(); }); - QObject::connect(ui->useCustomBrowser, &QCheckBox::clicked, [&] { - updateCustomBrowser(); - }); - QObject::connect(ui->browseCustomBrowser, &QPushButton::clicked, [&] { - browseCustomBrowser(); - }); - updateNexusData(); - updateCustomBrowser(); } void NexusSettingsTab::update() @@ -495,23 +487,3 @@ void NexusSettingsTab::updateNexusData() } } -void NexusSettingsTab::updateCustomBrowser() -{ - ui->browserCommand->setEnabled(ui->useCustomBrowser->isChecked()); -} - -void NexusSettingsTab::browseCustomBrowser() -{ - const QString Filters = - QObject::tr("Executables (*.exe)") + ";;" + QObject::tr("All Files (*.*)"); - - QString file = QFileDialog::getOpenFileName( - parentWidget(), QObject::tr("Select the browser executable"), - ui->browserCommand->text(), Filters); - - if (file.isNull() || file == "") { - return; - } - - ui->browserCommand->setText(file + " \"%1\""); -} diff --git a/src/src/settingsdialognexus.h b/src/src/settingsdialognexus.h index 8b7b56c..e43a318 100644 --- a/src/src/settingsdialognexus.h +++ b/src/src/settingsdialognexus.h @@ -71,8 +71,6 @@ private: void associate(); void updateNexusData(); - void updateCustomBrowser(); - void browseCustomBrowser(); }; #endif // SETTINGSDIALOGNEXUS_H diff --git a/src/src/settingsdialogworkarounds.cpp b/src/src/settingsdialogworkarounds.cpp deleted file mode 100644 index 2b252b3..0000000 --- a/src/src/settingsdialogworkarounds.cpp +++ /dev/null @@ -1,238 +0,0 @@ -#include "settingsdialogworkarounds.h" -#include "settings.h" -#include "spawn.h" -#include "ui_settingsdialog.h" -#include <iplugingame.h> -#include <log.h> -#include <report.h> - -WorkaroundsSettingsTab::WorkaroundsSettingsTab(Settings& s, SettingsDialog& d) - : SettingsTab(s, d) -{ - // options - ui->forceEnableBox->setChecked(settings().game().forceEnableCoreFiles()); - ui->enableArchiveParsingBox->setChecked(settings().archiveParsing()); - - // steam - QString username; - QString password; - settings().steam().login(username, password); - if (username.length() > 0) - MOBase::log::getDefault().addToBlacklist(username.toStdString(), "STEAM_USERNAME"); - if (password.length() > 0) - MOBase::log::getDefault().addToBlacklist(password.toStdString(), "STEAM_PASSWORD"); - - ui->appIDEdit->setText(settings().steam().appID()); - ui->steamUserEdit->setText(username); - ui->steamPassEdit->setText(password); - - // network - ui->offlineBox->setChecked(settings().network().offlineMode()); - ui->proxyBox->setChecked(settings().network().useProxy()); - ui->useCustomBrowser->setChecked(settings().network().useCustomBrowser()); - ui->browserCommand->setText(settings().network().customBrowserCommand()); - - // buttons - m_ExecutableBlacklist = settings().executablesBlacklist(); - m_SkipFileSuffixes = settings().skipFileSuffixes(); - m_SkipDirectories = settings().skipDirectories(); - - QObject::connect(ui->bsaDateBtn, &QPushButton::clicked, [&] { - on_bsaDateBtn_clicked(); - }); - QObject::connect(ui->execBlacklistBtn, &QPushButton::clicked, [&] { - on_execBlacklistBtn_clicked(); - }); - QObject::connect(ui->skipFileSuffixBtn, &QPushButton::clicked, [&] { - on_skipFileSuffixBtn_clicked(); - }); - QObject::connect(ui->skipDirectoriesBtn, &QPushButton::clicked, [&] { - on_skipDirectoriesBtn_clicked(); - }); - QObject::connect(ui->resetGeometryBtn, &QPushButton::clicked, [&] { - on_resetGeometryBtn_clicked(); - }); -} - -void WorkaroundsSettingsTab::update() -{ - // options - settings().game().setForceEnableCoreFiles(ui->forceEnableBox->isChecked()); - settings().setArchiveParsing(ui->enableArchiveParsingBox->isChecked()); - - // steam - if (ui->appIDEdit->text() != settings().game().plugin()->steamAPPId()) { - settings().steam().setAppID(ui->appIDEdit->text()); - } else { - settings().steam().setAppID(""); - } - settings().steam().setLogin(ui->steamUserEdit->text(), ui->steamPassEdit->text()); - - // network - settings().network().setOfflineMode(ui->offlineBox->isChecked()); - settings().network().setUseProxy(ui->proxyBox->isChecked()); - settings().network().setUseCustomBrowser(ui->useCustomBrowser->isChecked()); - settings().network().setCustomBrowserCommand(ui->browserCommand->text()); - - // buttons - settings().setExecutablesBlacklist(m_ExecutableBlacklist); - settings().setSkipFileSuffixes(m_SkipFileSuffixes); - settings().setSkipDirectories(m_SkipDirectories); -} - -bool WorkaroundsSettingsTab::changeBlacklistNow(QWidget* parent, Settings& settings) -{ - const auto current = settings.executablesBlacklist(); - - if (auto s = changeBlacklistLater(parent, current)) { - settings.setExecutablesBlacklist(*s); - return true; - } - - return false; -} - -std::optional<QString> -WorkaroundsSettingsTab::changeBlacklistLater(QWidget* parent, const QString& current) -{ - bool ok = false; - - QString result = QInputDialog::getMultiLineText( - parent, QObject::tr("Executables Blacklist"), - QObject::tr("Enter one executable per line to be blacklisted from the virtual " - "file system.\n" - "Mods and other virtualized files will not be visible to these " - "executables and\n" - "any executables launched by them.\n\n" - "Example:\n" - " Chrome.exe\n" - " Firefox.exe"), - current.split(";").join("\n"), &ok); - - if (!ok) { - return {}; - } - - QStringList blacklist; - for (auto exec : result.split("\n")) { - if (exec.trimmed().endsWith(".exe", Qt::CaseInsensitive)) { - blacklist << exec.trimmed(); - } - } - - return blacklist.join(";"); -} - -std::optional<QStringList> -WorkaroundsSettingsTab::changeSkipFileSuffixes(QWidget* parent, - const QStringList& current) -{ - bool ok = false; - - QString result = QInputDialog::getMultiLineText( - parent, QObject::tr("Skip File Suffixes"), - QObject::tr( - "Enter one file suffix per line to be skipped / ignored from the virtual " - "file system.\n" - "Not to be confused with file extensions, file suffixes are simply how the " - "filename ends.\n\n" - "Example:\n" - " .txt - Would skip all files that end with .txt, <any text>.txt\n" - " some_file.txt - Would skip all files that end with some_file.txt, <any " - "text>some_file.txt"), - current.join("\n"), &ok); - - if (!ok) { - return {}; - } - - QStringList fileSuffixes; - for (auto& suffix : result.split("\n")) { - auto trimmed = suffix.trimmed(); - if (!trimmed.isEmpty()) { - fileSuffixes << trimmed; - } - } - - return fileSuffixes; -} - -std::optional<QStringList> -WorkaroundsSettingsTab::changeSkipDirectories(QWidget* parent, - const QStringList& current) -{ - bool ok = false; - - QString result = QInputDialog::getMultiLineText( - parent, QObject::tr("Skip Directories"), - QObject::tr( - "Enter one directory per line to be skipped / ignored from the virtual " - "file system.\n\n" - "Example:\n" - " .git\n" - " instructions"), - current.join("\n"), &ok); - - if (!ok) { - return {}; - } - - QStringList directories; - for (auto& dir : result.split("\n")) { - auto trimmed = dir.trimmed(); - if (!trimmed.isEmpty()) { - directories << trimmed; - } - } - - return directories; -} - -void WorkaroundsSettingsTab::on_execBlacklistBtn_clicked() -{ - if (auto s = changeBlacklistLater(parentWidget(), m_ExecutableBlacklist)) { - m_ExecutableBlacklist = *s; - } -} - -void WorkaroundsSettingsTab::on_skipFileSuffixBtn_clicked() -{ - if (auto s = changeSkipFileSuffixes(parentWidget(), m_SkipFileSuffixes)) { - m_SkipFileSuffixes = *s; - } -} - -void WorkaroundsSettingsTab::on_skipDirectoriesBtn_clicked() -{ - if (auto s = changeSkipDirectories(parentWidget(), m_SkipDirectories)) { - m_SkipDirectories = *s; - } -} - -void WorkaroundsSettingsTab::on_bsaDateBtn_clicked() -{ - const auto* game = qApp->property("managed_game").value<MOBase::IPluginGame*>(); - // BSA backdating used the Win32 helper.exe for admin rights — the equivalent - // on Linux would just touch the files but it's never been wired up. - QDir dir = game->dataDirectory(); - Q_UNUSED(dir); -} - -void WorkaroundsSettingsTab::on_resetGeometryBtn_clicked() -{ - const auto r = - MOBase::TaskDialog(parentWidget()) - .title(QObject::tr("Restart Mod Organizer")) - .main(QObject::tr("Restart Mod Organizer")) - .content(QObject::tr("Geometries will be reset to their default values.")) - .icon(QMessageBox::Question) - .button({QObject::tr("Restart Mod Organizer"), QMessageBox::Ok}) - .button({QObject::tr("Cancel"), QMessageBox::Cancel}) - .exec(); - - if (r == QMessageBox::Ok) { - settings().geometry().requestReset(); - ExitModOrganizer(Exit::Restart); - dialog().close(); - } -} diff --git a/src/src/settingsdialogworkarounds.h b/src/src/settingsdialogworkarounds.h deleted file mode 100644 index 03561b4..0000000 --- a/src/src/settingsdialogworkarounds.h +++ /dev/null @@ -1,49 +0,0 @@ -#ifndef SETTINGSDIALOGWORKAROUNDS_H
-#define SETTINGSDIALOGWORKAROUNDS_H
-
-#include "settings.h"
-#include "settingsdialog.h"
-
-class WorkaroundsSettingsTab : public SettingsTab
-{
-public:
- WorkaroundsSettingsTab(Settings& settings, SettingsDialog& dialog);
-
- // shows the blacklist dialog from the given settings, and changes the
- // settings when the user accepts it
- //
- static bool changeBlacklistNow(QWidget* parent, Settings& settings);
-
- // shows the blacklist dialog from the given string and returns the new
- // blacklist if the user accepted it
- //
- static std::optional<QString> changeBlacklistLater(QWidget* parent,
- const QString& current);
-
- // shows the blacklist dialog from the given string and returns the new
- // blacklist if the user accepted it
- //
- static std::optional<QStringList> changeSkipFileSuffixes(QWidget* parent,
- const QStringList& current);
-
- // shows the blacklist dialog from the given string and returns the new
- // blacklist if the user accepted it
- //
- static std::optional<QStringList> changeSkipDirectories(QWidget* parent,
- const QStringList& current);
-
- void update() override;
-
-private:
- QString m_ExecutableBlacklist;
- QStringList m_SkipFileSuffixes;
- QStringList m_SkipDirectories;
-
- static void on_bsaDateBtn_clicked();
- void on_execBlacklistBtn_clicked();
- void on_skipFileSuffixBtn_clicked();
- void on_skipDirectoriesBtn_clicked();
- void on_resetGeometryBtn_clicked();
-};
-
-#endif // SETTINGSDIALOGWORKAROUNDS_H
diff --git a/src/src/spawn.cpp b/src/src/spawn.cpp index 76d4e0e..c6f4691 100644 --- a/src/src/spawn.cpp +++ b/src/src/spawn.cpp @@ -24,7 +24,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "fluorineconfig.h" #include "protonlauncher.h" #include "settings.h" -#include "settingsdialogworkarounds.h" #include "shared/appconfig.h" #include <QApplication> @@ -53,7 +52,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. class QMessageBox; using namespace MOBase; -using namespace MOShared; namespace spawn::dialogs { @@ -153,25 +151,16 @@ QMessageBox::StandardButton confirmBlacklisted(QWidget* parent, const auto details = "Executable: " + sp.binary.fileName() + "\nCurrent blacklist: " + settings.executablesBlacklist(); - auto r = MOBase::TaskDialog(parent, title) - .main(mainText) - .content(content) - .details(details) - .icon(QMessageBox::Question) - .remember("blacklistedExecutable", sp.binary.fileName()) - .button({QObject::tr("Continue"), - QObject::tr("Your mods might not work."), QMessageBox::Yes}) - .button({QObject::tr("Change the blacklist"), QMessageBox::Retry}) - .button({QObject::tr("Cancel"), QMessageBox::Cancel}) - .exec(); - - if (r == QMessageBox::Retry) { - if (!WorkaroundsSettingsTab::changeBlacklistNow(parent, settings)) { - r = QMessageBox::Cancel; - } - } - - return r; + return MOBase::TaskDialog(parent, title) + .main(mainText) + .content(content) + .details(details) + .icon(QMessageBox::Question) + .remember("blacklistedExecutable", sp.binary.fileName()) + .button({QObject::tr("Continue"), + QObject::tr("Your mods might not work."), QMessageBox::Yes}) + .button({QObject::tr("Cancel"), QMessageBox::Cancel}) + .exec(); } } // namespace spawn::dialogs diff --git a/src/src/systemtraymanager.cpp b/src/src/systemtraymanager.cpp index ad96c7d..498d29c 100644 --- a/src/src/systemtraymanager.cpp +++ b/src/src/systemtraymanager.cpp @@ -67,6 +67,21 @@ void SystemTrayManager::restoreFromSystemTray() }
}
+void SystemTrayManager::showNotification(const QString& title, const QString& message,
+ QSystemTrayIcon::MessageIcon icon)
+{
+ // The tray icon is normally only visible while minimized; show it just long
+ // enough for the notification to fire so the OS can route the message.
+ const bool wasVisible = m_SystemTrayIcon->isVisible();
+ if (!wasVisible) {
+ m_SystemTrayIcon->show();
+ }
+ m_SystemTrayIcon->showMessage(title, message, icon);
+ if (!wasVisible && !m_Parent->isHidden()) {
+ m_SystemTrayIcon->hide();
+ }
+}
+
void SystemTrayManager::on_systemTrayIcon_activated(
QSystemTrayIcon::ActivationReason reason)
{
diff --git a/src/src/systemtraymanager.h b/src/src/systemtraymanager.h index f46cd7a..5530a2a 100644 --- a/src/src/systemtraymanager.h +++ b/src/src/systemtraymanager.h @@ -35,6 +35,10 @@ public: void minimizeToSystemTray();
void restoreFromSystemTray();
+ void showNotification(
+ const QString& title, const QString& message,
+ QSystemTrayIcon::MessageIcon icon = QSystemTrayIcon::MessageIcon::Information);
+
private:
QMainWindow* m_Parent;
QDockWidget* m_LogDock;
|
