Compare commits

...
Author SHA1 Message Date
Jonathan Feenstra d4ce82a262 Extract getValidGameShortName method in download manager (#2380) 2026-05-04 13:39:08 +02:00
AL 4242abeab1 Guard the .meta creation in openMetaFile against the directory watcher
openMetaFile creates the .meta file via QSettings when one does not
exist; the disk write fires directoryChanged and triggers a spurious
refreshList. Wrap it in a DirWatcherManager::Guard like the other
meta-file editing paths.
2026-05-04 13:39:08 +02:00
AL 7d8d4bb545 Coalesce the removeDownload reset with the following refreshList
Moves the ModelResetGuard out of the try-catch so it also wraps the
refreshList() call below. Without this, one reset fires when the guard
destructs at the end of the try block and another fires from
refreshList's own guard, producing two resets where one is sufficient.
2026-05-04 13:39:08 +02:00
AL af789246f9 Address PR feedback: fix redundant check and move refresh outside try catch. 2026-05-04 13:39:08 +02:00
AL 6b5ffc41f4 Remove unused alphabetical translation vector
m_AlphabeticalTranslation was written but never read; drop it along with
refreshAlphabeticalTranslation, ByName, and the LessThanWrapper helper.
2026-05-04 13:39:07 +02:00
pre-commit-ci[bot] 5a44a6d934 [pre-commit.ci] Auto fixes from pre-commit.com hooks. 2026-05-04 13:39:07 +02:00
AL 78f4343304 Sanitize suffix path in getDownloadFileName
The collision-avoidance branch was using the raw baseName, so invalid
characters sanitized out of the initial path leaked into the suffixed one.
2026-05-04 13:39:07 +02:00
AL 6eb5191653 Fix memory leak in DownloadInfo::createFromMeta
Move the allocation past the early-return checks so path-mismatch and
hidden-skip paths no longer leak a fresh DownloadInfo.
2026-05-04 13:39:07 +02:00
AL 1e8ada5348 Fix lost finished() signal on fast downloads
Hoist the file-exists prompt out of startDownload so setup is straight-line.
Connect finished() last and dispatch manually if the reply already finished.
2026-05-04 13:39:07 +02:00
AL f7bb36b1b1 Fix comma operator in addNXMDownload pending-dedup check
The game-name comparison result was discarded by the comma operator,
so the dedup only matched modId/fileId across all games.
2026-05-04 13:39:07 +02:00
AL c94858be4d Centralize row notifications in setState and fix missed emits
setState emits notifyRowChanged itself, uses indexByInfo (-1 when
untracked), and re-looks up the row at each use so reply->abort() and
plugin callbacks that re-enter and erase info don't produce stale
signals.

Remove the trailing emit loop from createMetaFile and the now-redundant
notifyRowChanged calls scattered after setState. Add the two missing
emits in restoreDownload (after m_Hidden) and metaDataChanged (after
rename). Guard downloadFinished with a top-level DirWatcherGuard to
prevent filesystem events from its writes racing with model updates.
2026-05-04 13:39:07 +02:00
AL 07faf9c635 Replace aboutToUpdate/update(int) with ModelResetGuard
Replace the fragile two-signal protocol with a refcounted RAII
ModelResetGuard. Split update(int) into aboutToResetModel/modelReset
(guard only) and rowChanged(int); notifyRowChanged() is suppressed while
a reset is active.

Fixes "beginResetModel without endResetModel" warnings from three sites
in downloadFinished/removeDownload that were pairing reset with a row
update. removePending only opens a guard when an actual match is removed.
2026-05-04 13:39:07 +02:00
AL 829ee4f9c2 Encapsulate the downloads directory watcher in DirWatcherManager
QFileSystemWatcher suppression currently relies on public static start/end
methods and a static counter. Seven call sites pair them raw, one of them
outside the class. Any exception between a pair permanently disables the
watcher, and the static counter implies a singleton DownloadManager.

A new DirWatcherManager owns the watcher, the counter (now an instance
member), and the filtering. The only way to suspend is an RAII Guard
obtained via a scopedGuard() factory. All raw pairs migrate to guards. A
TODO flags the existing processEvents() in the dtor as a known reentrancy
hazard worth replacing later.
2026-05-04 13:39:06 +02:00
Al ca7a149874 Improve mod update check accuracy (#2385)
* Index API response into lookup maps

* extract new findLatestActiveSuccessor method from update check function

* extract method to check if a file is active

* use new isActiveFileStatus in modInfoRegular

* fix bug when merging during mod install

* remember ordering of installed nexus file ids

* refactor update check logic to prioritize Nexus file IDs over filenames

* refactor nxmUpdatesAvailable to simplify update checking logic

* refactor update check logic to find Nexus file IDs by filename and streamline successor retrieval

* refactor update checking to streamline version resolution and improve successor retrieval
2026-05-03 21:47:20 -05:00
Jeremy Rimpo ef7499aade Extended MODL / direct download handling (#2384)
* Extended MODL / direct download handling
- name, modname, version, and source options added to download command
- nxmhandler init adds schemas and MODL entry with default launch args
- Add MODL register button to general tab
- On window display, call meta function to trigger both registrations
2026-05-03 03:04:49 -05:00
Jeremy Rimpo 2e393aa3cc Handle Nexus collections links (#2383)
- Pops up dialog when the NXM link is a collection
- Collection link data available, though still unsupported
2026-05-02 10:45:09 -05:00
Mick dc420a258a Change IconDelegate::paintIcons to only execute when iconWidth > 0 (#2362) 2026-04-27 08:58:17 +02:00
Jonathan Feenstra 49da80c2a4 Make command-line arguments -i "" launch the portable instance (#2341) 2026-04-27 08:57:00 +02:00
Al f80ad0435c fix crash when CWD is not set to app directory (#2379) 2026-04-26 21:43:16 -05:00
Jeremy Rimpo 925bade315 Disabling tutorials (#2366) 2026-04-17 10:17:59 +02:00
Jonathan Feenstra 9deaf71362 Add more contributors to the about dialog (#2369) 2026-04-17 10:17:11 +02:00
Jeremy Rimpo 3a5140bb8f Starfield: Updated blueprint / blueprint prefix support (#2368)
* Add blueprint handling with blueprintships

* Blueprint changes
- Add tooltips
- Add warnings
- Handle blueprint prefixes properly

* Make sure we're assigning the property
2026-04-16 10:20:50 -05:00
Jeremy Rimpo 05593c0347 Update libloot dll name in sanity check (#2370) 2026-04-16 10:17:13 -05:00
Jeremy Rimpo ca4e81ca86 Removing references to openssl (#2367) 2026-04-16 14:09:53 +02:00
Jeremy Rimpo 6bd8bcc239 Update vcpkg targets (#2365) 2026-04-15 09:59:11 +02:00
Mick 7528d023c4 fix header guard typos (#2364) 2026-04-15 09:27:38 +02:00
pre-commit-ci[bot] 662f033295 [pre-commit.ci] Pre-commit autoupdate. (#2357)
* [pre-commit.ci] Pre-commit autoupdate.

updates:
- [github.com/pre-commit/mirrors-clang-format: v21.1.8 → v22.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.8...v22.1.2)

* [pre-commit.ci] Auto fixes from pre-commit.com hooks.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-04-09 13:16:16 +02:00
Jonathan Feenstra 2043d9931c Remove "Categories: " tooltips when there are no categories (#2339) 2026-02-18 19:48:12 +01:00
Jonathan Feenstra 4da0bffeee Add instance manager to plugin API (#2335) 2026-02-08 10:08:10 +01:00
Jonathan Feenstra a394f02e97 Add executables list to plugin API (#2327) 2026-01-31 16:49:46 +01:00
Jonathan Feenstra aa44561e86 Change IOrganizer::profile return type to a shared_ptr (#2322) 2026-01-11 17:31:26 +01:00
Jonathan Feenstra 717b5ac389 Add instanceName and profiles methods to plugin API (#2321) 2026-01-11 12:36:07 +01:00
pre-commit-ci[bot] 1505519ecd [pre-commit.ci] Pre-commit autoupdate. (#2320)
updates:
- [github.com/pre-commit/mirrors-clang-format: v21.1.2 → v21.1.8](https://github.com/pre-commit/mirrors-clang-format/compare/v21.1.2...v21.1.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-01-05 19:38:47 +01:00
Jonathan Feenstra e5ac1cc82d Add executable setting to minimize MO2 to the system tray while running (#2313) 2026-01-03 15:48:06 +01:00
Mikaël Capelle d5bd9603c6 Add missing Qt dependencies in CI. (#2317) 2026-01-03 14:49:24 +01:00
pre-commit-ci[bot] cc78137eb5 [pre-commit.ci] Pre-commit autoupdate. (#2295)
updates:
- [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0)
- [github.com/pre-commit/mirrors-clang-format: v20.1.7 → v21.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v20.1.7...v21.1.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-10-07 15:01:47 +02:00
itch 8016e77723 fix(qt): remove obsolete call to AA_EnableHighDpiScaling (#2283) 2025-09-15 09:40:50 +02:00
Mikaël Capelle b77b2722b7 Fix an issue with implementation of CombinedModDataContent. (#2278) 2025-09-03 10:13:06 +02:00
Jonathan Feenstra d52fcccb83 Add author and uploader columns to mod list (#2269) 2025-08-18 07:55:52 +02:00
Mikaël Capelle 6c64236e2e Allow non-cache build in CI when Azure variables are not available. (#2270) 2025-08-17 12:32:23 +02:00
pre-commit-ci[bot] 95b9ab2e45 [pre-commit.ci] Pre-commit autoupdate. (#2258)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.5 → v20.1.7](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.5...v20.1.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-08 07:50:31 +02:00
Mikaël Capelle 28e712c8a3 Add version of MO2 plugin loaded in logs. (#2252) 2025-06-06 15:06:11 +02:00
84 changed files with 2152 additions and 724 deletions
+2 -2
View File
@@ -7,7 +7,7 @@ on:
types: [opened, synchronize, reopened]
env:
VCPKG_BINARY_SOURCES: clear;x-azblob,${{ vars.AZ_BLOB_VCPKG_URL }},${{ secrets.AZ_BLOB_SAS }},readwrite
VCPKG_BINARY_SOURCES: ${{ vars.AZ_BLOB_VCPKG_URL != '' && format('clear;x-azblob,{0},{1},readwrite', vars.AZ_BLOB_VCPKG_URL, secrets.AZ_BLOB_SAS) || '' }}
jobs:
build:
@@ -17,7 +17,7 @@ jobs:
id: build-modorganizer
uses: ModOrganizer2/build-with-mob-action@master
with:
qt-modules: qtpositioning qtwebchannel qtwebengine qtwebsockets
qt-modules: qtpositioning qtwebchannel qtwebengine qtwebsockets qtnetworkauth
mo2-dependencies: usvfs uibase bsatk esptk archive lootcli
- name: Install ModOrganizer
+2 -2
View File
@@ -1,13 +1,13 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-case-conflict
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.5
rev: v22.1.2
hooks:
- id: clang-format
'types_or': [c++, c]
+3
View File
@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.16)
# TODO: clean include directives
set(MO2_CMAKE_DEPRECATED_UIBASE_INCLUDE ON)
# Remove tutorials until Qt is fixed
set(MO2_SKIP_TUTORIALS_INSTALL ON)
project(organizer)
# if MO2_INSTALL_IS_BIN is set, this means that we should install directly into the
+3
View File
@@ -109,6 +109,7 @@ mo2_add_filter(NAME src/application GROUPS
multiprocess
sanitychecks
selfupdater
systemtraymanager
updatedialog
)
@@ -273,7 +274,9 @@ mo2_add_filter(NAME src/profiles GROUPS
mo2_add_filter(NAME src/proxies GROUPS
downloadmanagerproxy
executableslistproxy
gamefeaturesproxy
instancemanagerproxy
modlistproxy
organizerproxy
pluginlistproxy
-2
View File
@@ -45,7 +45,6 @@ AboutDialog::AboutDialog(const QString& version, QWidget* parent)
m_LicenseFiles[LICENSE_CCBY3] = "BY-SA-v3.0.txt";
m_LicenseFiles[LICENSE_ZLIB] = "zlib.txt";
m_LicenseFiles[LICENSE_PYTHON] = "python.txt";
m_LicenseFiles[LICENSE_SSL] = "openssl.txt";
m_LicenseFiles[LICENSE_CPPTOML] = "cpptoml.txt";
m_LicenseFiles[LICENSE_UDIS] = "udis86.txt";
m_LicenseFiles[LICENSE_SPDLOG] = "spdlog.txt";
@@ -69,7 +68,6 @@ AboutDialog::AboutDialog(const QString& version, QWidget* parent)
addLicense("ANTLR", LICENSE_ANTLR);
addLicense("LOOT", LICENSE_GPL3);
addLicense("Python", LICENSE_PYTHON);
addLicense("OpenSSL", LICENSE_SSL);
addLicense("cpptoml", LICENSE_CPPTOML);
addLicense("Udis86", LICENSE_UDIS);
addLicense("spdlog", LICENSE_SPDLOG);
-1
View File
@@ -51,7 +51,6 @@ private:
LICENSE_BOOST,
LICENSE_CCBY3,
LICENSE_PYTHON,
LICENSE_SSL,
LICENSE_CPPTOML,
LICENSE_7ZIP,
LICENSE_ZLIB,
+167 -12
View File
@@ -127,7 +127,7 @@
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Copyright © 2011-2016 Sebastian Herbord&lt;br/&gt;Copyright © 2016-2025 Mod Organizer 2 Contributors&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string notr="true">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Copyright © 2011-2016 Sebastian Herbord&lt;br/&gt;Copyright © 2016-2026 Mod Organizer 2 Contributors&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
@@ -222,7 +222,7 @@
</item>
<item>
<property name="text">
<string>Holt59</string>
<string notr="true">Holt59</string>
</property>
</item>
</widget>
@@ -484,6 +484,21 @@
<string notr="true">6788</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AkiraJkr</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">AmeliaCute</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ashemedai</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">blacksol</string>
@@ -504,6 +519,16 @@
<string notr="true">Brixified</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">CheffieGithub</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">daescha</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ddbb07</string>
@@ -514,11 +539,31 @@
<string notr="true">deathneko11</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Deewens</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">DefinitelyNotSade</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">dekart811</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Deorder</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">diegofesanto</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">DoubleYou</string>
@@ -529,6 +574,11 @@
<string notr="true">Drew Warwick</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Eddoursul</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">erri120</string>
@@ -579,11 +629,66 @@
<string notr="true">jimfcarroll</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Jonny_Bro</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ju5tA1ex</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Kane Dou</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">KenJyn76</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Ketsuban</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">LaughingHyena</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Luca|EzioTheDeadPoet</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">mick-lue</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ModZero</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">mopioid</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">mrudat</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Nikirack</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">ogrotten</string>
@@ -599,21 +704,46 @@
<string notr="true">Patchier</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Paynamia</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">PurpleFez</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Ra2-IFV</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">reedts</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">RodolfoFigueroa</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Ryan Young</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Schilduin</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">shellbj</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">SuperSandro2000</string>
@@ -621,17 +751,22 @@
</item>
<item>
<property name="text">
<string notr="true">Trosski</string>
<string notr="true">Syer10</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Uhuru</string>
<string notr="true">The Conceptionist</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Wolverine2710</string>
<string notr="true">TheForgotten69</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">TheUnlocked</string>
</property>
</item>
<item>
@@ -641,12 +776,7 @@
</item>
<item>
<property name="text">
<string notr="true">z929669</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Eddoursul</string>
<string notr="true">Trosski</string>
</property>
</item>
<item>
@@ -656,7 +786,32 @@
</item>
<item>
<property name="text">
<string notr="true">ashemedai</string>
<string notr="true">Uhuru</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">uwx</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Wolverine2710</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">xieve</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">z929669</string>
</property>
</item>
<item>
<property name="text">
<string notr="true">Zash</string>
</property>
</item>
</widget>
+2 -2
View File
@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ARCHIVEFILENETRY_H
#define ARCHIVEFILENTRY_H
#ifndef ARCHIVEFILETREE_H
#define ARCHIVEFILETREE_H
#include <archive/archive.h>
#include <uibase/ifiletree.h>
+49 -6
View File
@@ -10,6 +10,8 @@
#include <log.h>
#include <report.h>
#include <boost/optional/optional_io.hpp>
namespace cl
{
@@ -221,8 +223,12 @@ std::optional<int> CommandLine::runEarly()
std::optional<int> CommandLine::runPostApplication(MOApplication& a)
{
// handle -i with no arguments
if (m_vm.count("instance") && m_vm["instance"].as<std::string>() == "") {
const auto instanceArg = m_vm.find("instance");
if (instanceArg != m_vm.end() &&
!instanceArg->second.as<boost::optional<std::string>>().has_value()) {
// handle -i with no arguments (distinct from -i "", which will launch the
// portable instance if it exists, hence the use of boost::optional)
env::Console c;
if (auto i = InstanceManager::singleton().currentInstance()) {
@@ -314,7 +320,9 @@ void CommandLine::createOptions()
("logs", "duplicates the logs to stdout")
("instance,i", po::value<std::string>()->implicit_value(""),
("instance,i",
po::value<boost::optional<std::string>>()->implicit_value(
boost::none),
"use the given instance (defaults to last used)")
("profile,p", po::value<std::string>(),
@@ -402,8 +410,14 @@ std::optional<QString> CommandLine::instance() const
if (m_shortcut.isValid() && m_shortcut.hasInstance()) {
return m_shortcut.instanceName();
} else if (m_vm.count("instance")) {
return QString::fromStdString(m_vm["instance"].as<std::string>());
} else {
const auto instanceArg = m_vm.find("instance");
if (instanceArg != m_vm.end()) {
const auto& instanceVal = instanceArg->second.as<boost::optional<std::string>>();
if (instanceVal.has_value()) {
return QString::fromStdString(instanceVal.value());
}
}
}
return {};
@@ -839,6 +853,18 @@ Command::Meta DownloadFileCommand::meta() const
return {"download", "downloads a file", "URL", ""};
}
po::options_description DownloadFileCommand::getVisibleOptions() const
{
po::options_description d;
d.add_options()("name,n", po::value<std::string>(), "(optional) the download name")(
"modname,m", po::value<std::string>(), "(optional) the mod name")(
"version,v", po::value<std::string>(), "(optional) the download / mod version")(
"source,s", po::value<std::string>(), "(optional) the download source");
return d;
}
po::options_description DownloadFileCommand::getInternalOptions() const
{
po::options_description d;
@@ -865,16 +891,33 @@ bool DownloadFileCommand::canForwardToPrimary() const
std::optional<int> DownloadFileCommand::runPostOrganizer(OrganizerCore& core)
{
const QString url = QString::fromStdString(vm()["URL"].as<std::string>());
QString name, modName, version, source;
if (!url.startsWith("https://")) {
reportError(QObject::tr("Download URL must start with https://"));
return 1;
}
if (vm().count("name")) {
name = QString::fromStdString(vm()["name"].as<std::string>());
}
if (vm().count("modname")) {
modName = QString::fromStdString(vm()["modname"].as<std::string>());
}
if (vm().count("version")) {
version = QString::fromStdString(vm()["version"].as<std::string>());
}
if (vm().count("source")) {
source = QString::fromStdString(vm()["source"].as<std::string>());
}
log::debug("starting direct download from command line: {}", url.toStdString());
MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(),
false);
core.downloadManager()->startDownloadURLs(QStringList() << url);
core.downloadManager()->startDownloadURLWithMeta(url, name, modName, version, source);
return {};
}
+1
View File
@@ -212,6 +212,7 @@ class DownloadFileCommand : public Command
protected:
Meta meta() const override;
po::options_description getVisibleOptions() const override;
po::options_description getInternalOptions() const override;
po::positional_options_description getPositional() const override;
-2
View File
@@ -6,9 +6,7 @@
<file name="dxcompiler.dll" />
<file name="dxil.dll" />
<file name="libbsarchd.dll" />
<file name="libcrypto-3-x64.dll" />
<file name="liblz4.dll" />
<file name="libssl-3-x64.dll" />
<file name="opengl32sw.dll" />
<file name="Qt6Concurrentd.dll" />
<file name="Qt6Cored.dll" />
-2
View File
@@ -6,9 +6,7 @@
<file name="dxcompiler.dll" />
<file name="dxil.dll" />
<file name="libbsarch.dll" />
<file name="libcrypto-3-x64.dll" />
<file name="liblz4.dll" />
<file name="libssl-3-x64.dll" />
<file name="opengl32sw.dll" />
<file name="Qt6Concurrent.dll" />
<file name="Qt6Core.dll" />
+13 -8
View File
@@ -35,8 +35,10 @@ DownloadList::DownloadList(OrganizerCore& core, QObject* parent)
: QAbstractTableModel(parent), m_manager(*core.downloadManager()),
m_settings(core.settings())
{
connect(&m_manager, SIGNAL(update(int)), this, SLOT(update(int)));
connect(&m_manager, SIGNAL(aboutToUpdate()), this, SLOT(aboutToUpdate()));
connect(&m_manager, &DownloadManager::aboutToResetModel, this,
&DownloadList::onAboutToResetModel);
connect(&m_manager, &DownloadManager::modelReset, this, &DownloadList::onModelReset);
connect(&m_manager, &DownloadManager::rowChanged, this, &DownloadList::onRowChanged);
}
int DownloadList::rowCount(const QModelIndex& parent) const
@@ -239,16 +241,19 @@ QVariant DownloadList::data(const QModelIndex& index, int role) const
return QVariant();
}
void DownloadList::aboutToUpdate()
void DownloadList::onAboutToResetModel()
{
emit beginResetModel();
beginResetModel();
}
void DownloadList::update(int row)
void DownloadList::onModelReset()
{
if (row < 0)
emit endResetModel();
else if (row < this->rowCount())
endResetModel();
}
void DownloadList::onRowChanged(int row)
{
if (row < this->rowCount())
emit dataChanged(
this->index(row, 0, QModelIndex()),
this->index(row, this->columnCount(QModelIndex()) - 1, QModelIndex()));
+11 -7
View File
@@ -83,16 +83,20 @@ public:
//
bool lessThanPredicate(const QModelIndex& left, const QModelIndex& right);
public slots:
private slots:
/**
* @brief used to inform the model that data has changed
*
* @param row the row that changed. This can be negative to update the whole view
**/
void update(int row);
* @brief full reset (row count changed). Drops selection and scroll state.
*/
void onAboutToResetModel();
void onModelReset();
void aboutToUpdate();
/**
* @brief single-row data change (row count unchanged). Preserves view state.
*
* @param row the row that changed
*/
void onRowChanged(int row);
private:
DownloadManager& m_manager;
+12 -3
View File
@@ -231,14 +231,18 @@ void DownloadListView::onCustomContextMenu(const QPoint& point)
menu.addAction(tr("Install"), [=] {
issueInstall(row);
});
if (m_Manager->isInfoIncomplete(row))
if (m_Manager->isInfoIncomplete(row)) {
menu.addAction(tr("Query Info"), [=] {
issueQueryInfoMd5(row);
});
else
} else {
menu.addAction(tr("Visit on Nexus"), [=] {
issueVisitOnNexus(row);
});
menu.addAction(tr("Visit the uploader's profile"), [=] {
issueVisitUploaderProfile(row);
});
}
menu.addAction(tr("Open File"), [=] {
issueOpenFile(row);
});
@@ -329,7 +333,7 @@ void DownloadListView::keyPressEvent(QKeyEvent* event)
const int row = qobject_cast<QSortFilterProxyModel*>(model())
->mapToSource(currentIndex())
.row();
auto state = m_Manager->getState(row);
auto state = m_Manager->getState(row);
if (state >= DownloadManager::STATE_READY) {
if (event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) {
issueInstall(row);
@@ -408,6 +412,11 @@ void DownloadListView::issueVisitOnNexus(int index)
emit visitOnNexus(index);
}
void DownloadListView::issueVisitUploaderProfile(int index)
{
emit visitUploaderProfile(index);
}
void DownloadListView::issueOpenFile(int index)
{
emit openFile(index);
+2
View File
@@ -87,6 +87,7 @@ signals:
void pauseDownload(int index);
void resumeDownload(int index);
void visitOnNexus(int index);
void visitUploaderProfile(int index);
void openFile(int index);
void openMetaFile(int index);
void openInDownloadsFolder(int index);
@@ -105,6 +106,7 @@ private slots:
void issueRestoreToView(int index);
void issueRestoreToViewAll();
void issueVisitOnNexus(int index);
void issueVisitUploaderProfile(int index);
void issueOpenFile(int index);
void issueOpenMetaFile(int index);
void issueOpenInDownloadsFolder(int index);
+270 -244
View File
File diff suppressed because it is too large Load Diff
+117 -44
View File
@@ -52,6 +52,58 @@ class NexusInterface;
class PluginContainer;
class OrganizerCore;
/**
* @brief QFileSystemWatcher with a nestable RAII suspension scope.
*
* Forwards directoryChanged() only while no Guard is alive. Use a Guard to
* bracket filesystem writes that would otherwise trigger a spurious refresh.
*/
class DirWatcherManager : public QObject
{
Q_OBJECT
public:
explicit DirWatcherManager(QObject* parent = nullptr);
/// Set the directory being watched (replaces any previous path).
void setPath(const QString& path);
/// True while one or more Guards are alive.
bool isSuspended() const;
/**
* @brief RAII suspension guard. Nests safely; the only way to suspend
* forwarding.
*/
class [[nodiscard]] Guard
{
public:
explicit Guard(DirWatcherManager& manager);
~Guard();
Guard(const Guard&) = delete;
Guard& operator=(const Guard&) = delete;
Guard(Guard&&) = delete;
Guard& operator=(Guard&&) = delete;
private:
DirWatcherManager& m_manager;
};
/// Returns a suspension Guard bound to the caller's scope.
[[nodiscard]] Guard scopedGuard();
signals:
/// Emitted when the watched directory changes and no Guard is active.
void directoryChanged();
private slots:
void onDirectoryChanged(const QString&);
private:
QFileSystemWatcher m_watcher;
int m_suspendDepth = 0;
};
/*!
* \brief manages downloading of files and provides progress information for gui
*elements
@@ -61,6 +113,25 @@ class DownloadManager : public QObject
Q_OBJECT
public:
/**
* @brief RAII full-reset guard. Use when the row count changes; drops
* view selection/scroll state. Nests safely: inner guards coalesce into
* the outermost scope so only one reset is emitted.
*/
class [[nodiscard]] ModelResetGuard
{
public:
explicit ModelResetGuard(DownloadManager& manager);
~ModelResetGuard();
ModelResetGuard(const ModelResetGuard&) = delete;
ModelResetGuard& operator=(const ModelResetGuard&) = delete;
ModelResetGuard(ModelResetGuard&&) = delete;
ModelResetGuard& operator=(ModelResetGuard&&) = delete;
private:
DownloadManager& m_manager;
};
enum DownloadState
{
STATE_STARTED = 0,
@@ -191,19 +262,6 @@ public:
**/
void setOutputDirectory(const QString& outputDirectory, const bool refresh = true);
/**
* @brief disables feedback from the downlods fileSystemWhatcher untill
*disableDownloadsWatcherEnd() is called
*
**/
static void startDisableDirWatcher();
/**
* @brief re-enables feedback from the downlods fileSystemWhatcher after
*disableDownloadsWatcherStart() was called
**/
static void endDisableDirWatcher();
/**
* @return current download directory
**/
@@ -408,8 +466,17 @@ public:
*/
void queryDownloadListInfo();
/**
* @return the directory watcher for the downloads folder; call
* scopedGuard() on it to suspend across filesystem writes.
*/
DirWatcherManager& dirWatcher() { return m_DirWatcher; }
public: // IDownloadManager interface:
int startDownloadURLs(const QStringList& urls);
int startDownloadURLWithMeta(const QString& url, const QString& name,
const QString& modName, const QString& version,
const QString& source);
int startDownloadNexusFile(const QString& gameName, int modID, int fileID);
QString downloadPath(int id);
@@ -432,16 +499,38 @@ public: // IDownloadManager interface:
void pauseAll();
Q_SIGNALS:
void aboutToUpdate();
/**
* @brief signals that the specified download has changed
* @brief notify the UI that a single row's data changed. Preserves view
* state; prefer over ModelResetGuard when the row count is unchanged.
*
* @param row the row that changed. This corresponds to the download index
**/
void update(int row);
*/
void notifyRowChanged(int row);
Q_SIGNALS:
/**
* @brief emitted before the download list model is about to be reset
*
* Emitted by ModelResetGuard on construction. Views should call
* beginResetModel() in response.
*/
void aboutToResetModel();
/**
* @brief emitted after the download list model has been reset
*
* Emitted by ModelResetGuard on destruction. Views should call
* endResetModel() in response.
*/
void modelReset();
/**
* @brief signals that the specified download row's data has changed
*
* @param row the row that changed. This corresponds to the download index
*/
void rowChanged(int row);
/**
* @brief signals the ui that a message should be displayed
@@ -503,6 +592,8 @@ public slots:
void visitOnNexus(int index);
void visitUploaderProfile(int index);
void openFile(int index);
void openMetaFile(int index);
@@ -536,7 +627,6 @@ private slots:
void downloadFinished(int index = 0);
void downloadError(QNetworkReply::NetworkError error);
void metaDataChanged();
void directoryChanged(const QString& dirctory);
void checkDownloadTimeout();
private:
@@ -576,10 +666,6 @@ private:
void removeFile(int index, bool deleteFile);
void refreshAlphabeticalTranslation();
bool ByName(int LHS, int RHS);
QString getFileNameFromNetworkReply(QNetworkReply* reply);
void setState(DownloadInfo* info, DownloadManager::DownloadState state);
@@ -592,6 +678,8 @@ private:
void writeData(DownloadInfo* info);
QString getValidGameShortName(const QString& gameNexusName) const;
private:
static const int AUTOMATIC_RETRIES = 3;
@@ -607,22 +695,17 @@ private:
QString m_OutputDirectory;
std::set<int> m_RequestIDs;
QVector<int> m_AlphabeticalTranslation;
QFileSystemWatcher m_DirWatcher;
DirWatcherManager m_DirWatcher;
// nesting depth of active ModelResetGuard scopes; see its docs
int m_modelResetDepth = 0;
SignalDownloadCallback m_DownloadComplete;
SignalDownloadCallback m_DownloadPaused;
SignalDownloadCallback m_DownloadFailed;
SignalDownloadCallback m_DownloadRemoved;
// The dirWatcher is actually triggering off normal Mo operations such as deleting
// downloads or editing .meta files so it needs to be disabled during operations that
// are known to cause the creation or deletion of files in the Downloads folder.
// Notably using QSettings to edit a file creates a temporarily .lock file that causes
// the Watcher to trigger multiple listRefreshes freezing the ui.
static int m_DirWatcherDisabler;
std::map<QString, int> m_DownloadFails;
bool m_ShowHidden;
@@ -632,14 +715,4 @@ private:
QTimer m_TimeoutTimer;
};
class ScopedDisableDirWatcher
{
public:
ScopedDisableDirWatcher(DownloadManager* downloadManager);
~ScopedDisableDirWatcher();
private:
DownloadManager* m_downloadManager;
};
#endif // DOWNLOADMANAGER_H
+2
View File
@@ -37,6 +37,8 @@ DownloadsTab::DownloadsTab(OrganizerCore& core, Ui::MainWindow* mwui)
SLOT(queryInfoMd5(int)));
connect(ui.list, SIGNAL(visitOnNexus(int)), m_core.downloadManager(),
SLOT(visitOnNexus(int)));
connect(ui.list, SIGNAL(visitUploaderProfile(int)), m_core.downloadManager(),
SLOT(visitUploaderProfile(int)));
connect(ui.list, SIGNAL(openFile(int)), m_core.downloadManager(),
SLOT(openFile(int)));
connect(ui.list, SIGNAL(openMetaFile(int)), m_core.downloadManager(),
+16 -3
View File
@@ -118,6 +118,9 @@ EditExecutablesDialog::EditExecutablesDialog(OrganizerCore& oc, int sel,
connect(ui->useApplicationIcon, &QCheckBox::toggled, [&] {
save();
});
connect(ui->minimizeToSystemTray, &QCheckBox::toggled, [&] {
save();
});
connect(ui->hide, &QCheckBox::toggled, [&] {
save();
});
@@ -136,7 +139,7 @@ int EditExecutablesDialog::exec()
void EditExecutablesDialog::loadCustomOverwrites()
{
const auto* p = m_organizerCore.currentProfile();
const auto p = m_organizerCore.currentProfile();
for (const auto& e : m_executablesList) {
const auto s = p->setting("custom_overwrites", e.title()).toString();
@@ -149,7 +152,7 @@ void EditExecutablesDialog::loadCustomOverwrites()
void EditExecutablesDialog::loadForcedLibraries()
{
const auto* p = m_organizerCore.currentProfile();
const auto p = m_organizerCore.currentProfile();
for (const auto& e : m_executablesList) {
m_forcedLibraries.set(e.title(), p->forcedLibrariesEnabled(e.title()),
@@ -234,7 +237,7 @@ bool EditExecutablesDialog::commitChanges()
return false;
}
auto* profile = m_organizerCore.currentProfile();
auto profile = m_organizerCore.currentProfile();
// remove all the custom overwrites and forced libraries
for (const auto& e : m_originalExecutables) {
@@ -382,6 +385,8 @@ void EditExecutablesDialog::clearEdits()
ui->configureLibraries->setEnabled(false);
ui->useApplicationIcon->setEnabled(false);
ui->useApplicationIcon->setChecked(false);
ui->minimizeToSystemTray->setEnabled(false);
ui->minimizeToSystemTray->setChecked(false);
ui->hide->setEnabled(false);
ui->hide->setChecked(false);
@@ -398,6 +403,7 @@ void EditExecutablesDialog::setEdits(const Executable& e)
ui->steamAppID->setEnabled(!e.steamAppID().isEmpty());
ui->steamAppID->setText(e.steamAppID());
ui->useApplicationIcon->setChecked(e.usesOwnIcon());
ui->minimizeToSystemTray->setChecked(e.minimizeToSystemTray());
ui->hide->setChecked(e.hide());
m_lastGoodTitle = e.title();
@@ -443,6 +449,7 @@ void EditExecutablesDialog::setEdits(const Executable& e)
ui->useApplicationIcon->setEnabled(true);
ui->createFilesInMod->setEnabled(true);
ui->forceLoadLibraries->setEnabled(true);
ui->minimizeToSystemTray->setEnabled(true);
ui->hide->setEnabled(true);
}
@@ -503,6 +510,12 @@ void EditExecutablesDialog::save()
e->flags(e->flags() & (~Executable::UseApplicationIcon));
}
if (ui->minimizeToSystemTray->isChecked()) {
e->flags(e->flags() | Executable::MinimizeToSystemTray);
} else {
e->flags(e->flags() & (~Executable::MinimizeToSystemTray));
}
if (ui->hide->isChecked()) {
e->flags(e->flags() | Executable::Hide);
} else {

Some files were not shown because too many files have changed in this diff Show More