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
64 changed files with 3436 additions and 4646 deletions
+1 -4
View File
@@ -8,12 +8,9 @@ src/*.bak
CMakeLists.txt.user
edit
/CMakeFiles
.idea/*
!.idea/filetypes/
!.idea/filetypes/qt-translations.xml
.idea
/msbuild.log
/*std*.log
/*build
/src/version.aps
.idea/
+1 -1
View File
@@ -7,7 +7,7 @@ repos:
- id: check-merge-conflict
- id: check-case-conflict
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v22.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
+4 -2
View File
@@ -1,3 +1,5 @@
[![Build status](https://ci.appveyor.com/api/projects/status/hxenwxmpaob5xung?svg=true)](https://ci.appveyor.com/project/ModOrganizer2/modorganizer-736bd)
# Mod Organizer
Mod Organizer (MO) is a tool for managing mod collections of arbitrary size. It is specifically designed for people who like to experiment with mods and thus need an easy and reliable way to install and uninstall them.
@@ -18,14 +20,14 @@ If you want to submit your code changes, please use a good formatting style like
Through the work of a few people of the community MO2 has come quite far, now it needs some more of those people to go further.
## Reporting Issues:
Issues should be reported to the GitHub page or on the open Discord server: [Mod Organizer 2](https://discord.gg/ewUVAqyrQX). Here is also where dev builds are tested, bugs are reported and investigated, suggestions are discussed and a lot more.
Issues should be reported to the GitHub page or on the open discord server: [Mod Organizer 2](https://discord.gg/ewUVAqyrQX). Here is also where dev builds are tested, bugs are reported and investigated, suggestions are discussed and a lot more.
Credits to Tannin, LePresidente, Silarn, erasmux, AL12, LostDragonist, AnyOldName3, isa, Holt59, Project579, przester, Qudix, RJ, Jonathan Feenstra and many others for the development.
## Download Location
* on [GitHub.com](https://github.com/Modorganizer2/modorganizer/releases)
* on [NexusMods.com](https://www.nexusmods.com/site/mods/6)
* on [NexusMods.com](https://www.nexusmods.com/skyrimspecialedition/mods/6194)
## Old Download Location
+2 -5
View File
@@ -12,7 +12,7 @@ find_package(mo2-esptk CONFIG REQUIRED)
find_package(mo2-dds-header CONFIG REQUIRED)
find_package(mo2-libbsarch CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebSockets NetworkAuth)
find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebSockets)
find_package(Boost CONFIG REQUIRED COMPONENTS program_options thread interprocess signals2 uuid accumulators)
find_package(7zip CONFIG REQUIRED)
find_package(lz4 CONFIG REQUIRED)
@@ -41,7 +41,7 @@ target_link_libraries(organizer PRIVATE
usvfs::usvfs mo2::uibase mo2::archive mo2::libbsarch
mo2::bsatk mo2::esptk mo2::lootcli-header
Boost::program_options Boost::signals2 Boost::uuid Boost::accumulators
Qt6::WebEngineWidgets Qt6::WebSockets Qt6::NetworkAuth Version Dbghelp)
Qt6::WebEngineWidgets Qt6::WebSockets Version Dbghelp)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt6"
DESTINATION ${_bin}/dlls
@@ -130,9 +130,6 @@ mo2_add_filter(NAME src/core GROUPS
githubpp
installationmanager
nexusinterface
nexusoauthlogin
nexusoauthtokens
nexusoauthconfig
nxmaccessmanager
organizercore
game_features
+876 -893
View File
File diff suppressed because it is too large Load Diff
+4 -15
View File
@@ -19,17 +19,12 @@ APIUserAccount::APIUserAccount() : m_type(APIUserAccountTypes::None) {}
bool APIUserAccount::isValid() const
{
return !m_accessToken.isEmpty() || !m_apiKey.isEmpty();
}
const QString& APIUserAccount::accessToken() const
{
return m_accessToken;
return !m_key.isEmpty();
}
const QString& APIUserAccount::apiKey() const
{
return m_apiKey;
return m_key;
}
const QString& APIUserAccount::id() const
@@ -52,15 +47,9 @@ const APILimits& APIUserAccount::limits() const
return m_limits;
}
APIUserAccount& APIUserAccount::accessToken(const QString& token)
APIUserAccount& APIUserAccount::apiKey(const QString& key)
{
m_accessToken = token;
return *this;
}
APIUserAccount& APIUserAccount::apiKey(const QString& apiKey)
{
m_apiKey = apiKey;
m_key = key;
return *this;
}
+4 -14
View File
@@ -65,12 +65,7 @@ public:
bool isValid() const;
/**
* OAuth access token
*/
const QString& accessToken() const;
/**
* OAuth access token
* api key
*/
const QString& apiKey() const;
@@ -95,14 +90,9 @@ public:
const APILimits& limits() const;
/**
* sets the OAuth access token
* sets the api key
*/
APIUserAccount& accessToken(const QString& token);
/**
* sets the OAuth access token
*/
APIUserAccount& apiKey(const QString& apiKey);
APIUserAccount& apiKey(const QString& key);
/**
* sets the user id
@@ -142,7 +132,7 @@ public:
bool exhausted() const;
private:
QString m_accessToken, m_apiKey, m_id, m_name;
QString m_key, m_id, m_name;
APIUserAccountTypes m_type;
APILimits m_limits;
};
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="microsoft.com">true</longPathAware>
</windowsSettings>
</application>
</assembly>
+14 -20
View File
@@ -57,17 +57,13 @@ void CategoryFactory::loadCategories()
QFile categoryFile(categoriesFilePath());
bool needLoad = false;
if (!categoryFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
if (!categoryFile.open(QIODevice::ReadOnly)) {
needLoad = true;
} else {
const auto lines = categoryFile.readAll().split('\n');
categoryFile.close();
for (int lineNum = 0; lineNum < lines.size(); ++lineNum) {
const auto& line = lines[lineNum];
if (line.isEmpty()) {
continue;
}
int lineNum = 0;
while (!categoryFile.atEnd()) {
QByteArray line = categoryFile.readLine();
++lineNum;
QList<QByteArray> cells = line.split('|');
if (cells.count() == 4) {
std::vector<NexusCategory> nexusCats;
@@ -80,7 +76,7 @@ void CategoryFactory::loadCategories()
if (!ok) {
log::error(tr("invalid category id {0}"), iter->constData());
}
nexusCats.emplace_back("Unknown", temp);
nexusCats.push_back(NexusCategory("Unknown", temp));
}
}
bool cell0Ok = true;
@@ -107,19 +103,16 @@ void CategoryFactory::loadCategories()
line.constData(), cells.count());
}
}
categoryFile.close();
QFile nexusMapFile(nexusMappingFilePath());
if (!nexusMapFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
if (!nexusMapFile.open(QIODevice::ReadOnly)) {
needLoad = true;
} else {
const auto nexLines = nexusMapFile.readAll().split('\n');
nexusMapFile.close();
for (int nexLineNum = 0; nexLineNum < nexLines.size(); ++nexLineNum) {
const auto& nexLine = nexLines[nexLineNum];
if (nexLine.isEmpty()) {
continue;
}
int nexLineNum = 0;
while (!nexusMapFile.atEnd()) {
QByteArray nexLine = nexusMapFile.readLine();
++nexLineNum;
QList<QByteArray> nexCells = nexLine.split('|');
if (nexCells.count() == 3) {
std::vector<NexusCategory> nexusCats;
@@ -136,11 +129,12 @@ void CategoryFactory::loadCategories()
m_NexusMap.insert_or_assign(nexID, NexusCategory(nexName, nexID));
m_NexusMap.at(nexID).setCategoryID(catID);
} else {
log::error(tr("invalid nexus category line {0}: {1} ({2} cells)"), nexLineNum,
log::error(tr("invalid nexus category line {0}: {1} ({2} cells)"), lineNum,
nexLine.constData(), nexCells.count());
}
}
}
nexusMapFile.close();
}
std::sort(m_Categories.begin(), m_Categories.end());
setParents();
+2 -9
View File
@@ -857,8 +857,7 @@ po::options_description DownloadFileCommand::getVisibleOptions() const
{
po::options_description d;
d.add_options()("game,g", po::value<std::string>(), "managed game")(
"name,n", po::value<std::string>(), "(optional) the download name")(
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");
@@ -892,7 +891,6 @@ bool DownloadFileCommand::canForwardToPrimary() const
std::optional<int> DownloadFileCommand::runPostOrganizer(OrganizerCore& core)
{
const QString url = QString::fromStdString(vm()["URL"].as<std::string>());
QString game("");
QString name, modName, version, source;
if (!url.startsWith("https://")) {
@@ -900,10 +898,6 @@ std::optional<int> DownloadFileCommand::runPostOrganizer(OrganizerCore& core)
return 1;
}
if (vm().count("game")) {
game = QString::fromStdString(vm()["game"].as<std::string>());
}
if (vm().count("name")) {
name = QString::fromStdString(vm()["name"].as<std::string>());
}
@@ -923,8 +917,7 @@ std::optional<int> DownloadFileCommand::runPostOrganizer(OrganizerCore& core)
log::debug("starting direct download from command line: {}", url.toStdString());
MessageDialog::showMessage(QObject::tr("Download started"), qApp->activeWindow(),
false);
core.downloadManager()->startDownloadURLWithMeta(url, game, name, modName, version,
source);
core.downloadManager()->startDownloadURLWithMeta(url, name, modName, version, source);
return {};
}
+1 -1
View File
@@ -27,7 +27,7 @@ class Settings;
//
// pages can be disabled if they return true in skip(), which happens globally
// for some (IntroPage has a setting in the registry), depending on context
// (NexusPage is skipped if the Nexus authorization already exists) or explicitly (when
// (NexusPage is skipped if the API key already exists) or explicitly (when
// only some info about the instance is missing on startup, such as a game
// variant)
//
+1 -2
View File
@@ -103,7 +103,6 @@ void Page::next()
bool Page::action(CreateInstanceDialog::Actions a)
{
Q_UNUSED(a);
// no-op
return false;
}
@@ -1204,7 +1203,7 @@ NexusPage::NexusPage(CreateInstanceDialog& dlg) : Page(dlg), m_skip(false)
// just check it once, or connecting and then going back and forth would skip
// the page, which would be unexpected
m_skip = GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey();
m_skip = GlobalSettings::hasNexusApiKey();
}
NexusPage::~NexusPage() = default;
-1
View File
@@ -12,7 +12,6 @@
<file name="Qt6Cored.dll" />
<file name="Qt6Guid.dll" />
<file name="Qt6Networkd.dll" />
<file name="Qt6NetworkAuthd.dll" />
<file name="Qt6OpenGLd.dll" />
<file name="Qt6OpenGLWidgetsd.dll" />
<file name="Qt6Positioningd.dll" />
-1
View File
@@ -12,7 +12,6 @@
<file name="Qt6Core.dll" />
<file name="Qt6Gui.dll" />
<file name="Qt6Network.dll" />
<file name="Qt6NetworkAuth.dll" />
<file name="Qt6OpenGL.dll" />
<file name="Qt6OpenGLWidgets.dll" />
<file name="Qt6Positioning.dll" />
+5 -7
View File
@@ -58,9 +58,7 @@ int DownloadList::columnCount(const QModelIndex&) const
QModelIndex DownloadList::index(int row, int column, const QModelIndex&) const
{
// Embed the stable DownloadID in internalId() so any consumer of the index
// can identify the download without having to track row shifts.
return createIndex(row, column, m_manager.downloadIDAtRow(row));
return createIndex(row, column, row);
}
QModelIndex DownloadList::parent(const QModelIndex&) const
@@ -114,14 +112,14 @@ QVariant DownloadList::data(const QModelIndex& index, int role) const
bool pendingDownload = index.row() >= m_manager.numTotalDownloads();
if (role == Qt::DisplayRole) {
if (pendingDownload) {
const DownloadManager::PendingDownload pending =
std::tuple<QString, int, int> nexusids =
m_manager.getPendingDownload(index.row() - m_manager.numTotalDownloads());
switch (index.column()) {
case COL_NAME:
return tr("< game %1 mod %2 file %3 >")
.arg(pending.gameName)
.arg(pending.modID)
.arg(pending.fileID);
.arg(std::get<0>(nexusids))
.arg(std::get<1>(nexusids))
.arg(std::get<2>(nexusids));
case COL_SIZE:
return tr("Unknown");
case COL_STATUS:
+272 -402
View File
File diff suppressed because it is too large Load Diff
+29 -136
View File
@@ -24,7 +24,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QElapsedTimer>
#include <QFile>
#include <QFileSystemWatcher>
#include <QHash>
#include <QMap>
#include <QNetworkReply>
#include <QObject>
@@ -32,6 +31,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QSettings>
#include <QStringList>
#include <QTime>
#include <QTimer>
#include <QUrl>
#include <QVector>
#include <boost/accumulators/accumulators.hpp>
@@ -40,7 +40,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <boost/signals2.hpp>
#include <idownloadmanager.h>
#include <modrepositoryfileinfo.h>
#include <optional>
#include <set>
using namespace boost::accumulators;
@@ -101,8 +100,6 @@ private slots:
void onDirectoryChanged(const QString&);
private:
void releaseSuspension();
QFileSystemWatcher m_watcher;
int m_suspendDepth = 0;
};
@@ -153,32 +150,6 @@ public:
STATE_UNINSTALLED
};
/**
* @brief Stable identifier for a download.
*
* Monotonically increasing within a session and never reused. Distinct from
* row indices, which are positional and shift as the list is mutated.
*/
using DownloadID = unsigned int;
/**
* @brief A download that has been requested but has not yet produced a
* DownloadInfo.
*
* Created when the user initiates an NXM download; drained either when the
* Nexus API returns the actual download URL (at which point a DownloadInfo
* is created using reservedID as its download id so that external references
* handed out before the download existed remain valid) or when the request
* is cancelled or fails.
*/
struct PendingDownload
{
QString gameName;
int modID;
int fileID;
DownloadID reservedID;
};
private:
struct DownloadInfo
{
@@ -187,7 +158,7 @@ private:
accumulator_set<qint64, stats<tag::rolling_mean>> m_DownloadTimeAcc;
qint64 m_DownloadLast;
qint64 m_DownloadTimeLast;
DownloadID m_DownloadID;
unsigned int m_DownloadID;
QString m_FileName;
QFile m_Output;
QNetworkReply* m_Reply;
@@ -216,26 +187,8 @@ private:
bool m_Hidden;
/**
* @brief Issue a new download id.
*
* The only supported way to obtain one; ids are monotonically increasing
* within a session and never reused.
*/
static DownloadID newDownloadID();
/**
* @brief Create a new DownloadInfo for a fresh download.
*
* When reservedID is provided it is used as the download id. Callers that
* need to hand out an id before the DownloadInfo exists (e.g. the NXM flow
* reserves an id when the request is queued, long before the Nexus API
* returns the actual URL) should reserve via newDownloadID() and pass it
* here. Otherwise a fresh id is drawn internally.
*/
static DownloadInfo* createNew(const MOBase::ModRepositoryFileInfo* fileInfo,
const QStringList& URLs,
std::optional<DownloadID> reservedID = {});
const QStringList& URLs);
static DownloadInfo* createFromMeta(const QString& filePath, bool showHidden,
const QString outputDirectory,
std::optional<uint64_t> fileSize = {});
@@ -250,14 +203,14 @@ private:
**/
void setName(QString newName, bool renameFile);
DownloadID downloadID() { return m_DownloadID; }
unsigned int downloadID() { return m_DownloadID; }
bool isPausedState();
QString currentURL();
private:
static DownloadID s_NextDownloadID;
static unsigned int s_NextDownloadID;
private:
DownloadInfo()
@@ -344,22 +297,18 @@ 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(),
std::optional<DownloadID> reservedID = {});
new MOBase::ModRepositoryFileInfo());
/**
* @brief start a download using a nxm-link
*
* Starts a download using a nxm-link. The download manager will first query the
* nexus page for file information. The returned id identifies the eventual
* download; it is reserved immediately so external references remain valid even
* before the Nexus API responds.
* starts a download using a nxm-link. The download manager will first query the nexus
* page for file information.
* @param url a nxm link looking like this: nxm://skyrim/mods/1234/files/4711
* @return the reserved download id
* @todo the game name encoded into the link is currently ignored, all downloads are
*incorrectly assumed to be for the identified game
**/
DownloadID addNXMDownload(const QString& url);
void addNXMDownload(const QString& url);
/**
* @brief retrieve the total number of downloads, both finished and unfinished
@@ -380,24 +329,9 @@ public:
* @brief retrieve the info of a pending download
* @param index index of the pending download (index in the range [0,
* numPendingDownloads()[)
* @return the PendingDownload entry at the given index
* @return pair of modid, fileid
*/
PendingDownload getPendingDownload(int index);
/**
* @brief Resolve a view row to a stable DownloadID.
*
* Rows cover active downloads followed by pending ones. Returns 0 if the row
* is out of range.
*/
DownloadID downloadIDAtRow(int row) const;
/**
* @brief Resolve a stable DownloadID to its current view row.
*
* @return the current row, or -1 if no download with that id is tracked.
*/
int rowForDownloadID(DownloadID id) const;
std::tuple<QString, int, int> getPendingDownload(int index);
/**
* @brief retrieve the full path to the download specified by index
@@ -540,9 +474,9 @@ public:
public: // IDownloadManager interface:
int startDownloadURLs(const QStringList& urls);
int startDownloadURLWithMeta(const QString& url, const QString& game,
const QString& name, const QString& modName,
const QString& version, const QString& source);
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);
@@ -644,13 +578,13 @@ public slots:
* @brief cancel the specified download. This will lead to the corresponding file to
*be deleted
*
* @param id id of the download to cancel
* @param index index of the download to cancel
**/
void cancelDownload(DownloadID id);
void cancelDownload(int index);
void pauseDownload(DownloadID id);
void pauseDownload(int index);
void resumeDownload(DownloadID id);
void resumeDownload(int index);
void queryInfo(int index);
@@ -690,23 +624,10 @@ private slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
void downloadReadyRead();
/**
* @brief Slot wired to QNetworkReply::finished().
*
* Resolves the originating reply through sender() and then dispatches to
* finishDownload. Use the public finishDownload directly for non-slot calls.
*/
void onReplyFinished();
/**
* @brief Run the post-download bookkeeping for the given download.
*
* Writes any remaining data, transitions the download's state, and emits
* the appropriate plugin signals.
*/
void finishDownload(DownloadID id);
void downloadFinished(int index = 0);
void downloadError(QNetworkReply::NetworkError error);
void metaDataChanged();
void checkDownloadTimeout();
private:
void createMetaFile(DownloadInfo* info);
@@ -725,15 +646,8 @@ public:
QString getDownloadFileName(const QString& baseName, bool rename = false) const;
private:
/**
* @brief Begin downloading into newDownload from reply.
*
* On the !resume path newDownload becomes owned by m_ActiveDownloads on
* success; on failure (e.g. the output file cannot be opened) it is deleted
* before returning. Returns whether the download actually started.
*/
bool startDownload(QNetworkReply* reply, DownloadInfo* newDownload, bool resume);
void resumeDownloadInt(DownloadID id);
void startDownload(QNetworkReply* reply, DownloadInfo* newDownload, bool resume);
void resumeDownloadInt(int index);
/**
* @brief start a download from a url
@@ -744,8 +658,7 @@ 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,
std::optional<DownloadID> reservedID = {});
const MOBase::ModRepositoryFileInfo* fileInfo);
// important: the caller has to lock the list-mutex, otherwise the
// DownloadInfo-pointer might get invalidated at any time
@@ -757,30 +670,10 @@ private:
void setState(DownloadInfo* info, DownloadManager::DownloadState state);
DownloadInfo* downloadInfoByID(DownloadID id);
QString displayNameByInfo(const DownloadInfo* info) const;
DownloadInfo* downloadInfoByID(unsigned int id);
void removePending(QString gameName, int modID, int fileID);
/**
* @brief Fire onDownloadFailed for a pending entry, if any matches.
*
* Used on Nexus API failures so callers holding a reserved id from
* addNXMDownload do not wait indefinitely for a result. No-op if no pending
* entry matches the (gameName, modID, fileID) triple.
*/
void notifyPendingDownloadFailed(const QString& gameName, int modID, int fileID);
/**
* @brief Roll back a download that has not yet been activated.
*
* Ensures a caller awaiting the reservedID receives an onDownloadFailed
* callback. Must not be called once the download has been registered as
* active.
*/
void cancelPendingDownload(DownloadInfo* newDownload, QNetworkReply* reply);
static QString getFileTypeString(int fileType);
void writeData(DownloadInfo* info);
@@ -796,14 +689,10 @@ private:
OrganizerCore* m_OrganizerCore;
QWidget* m_ParentWidget;
QVector<PendingDownload> m_PendingDownloads;
QVector<std::tuple<QString, int, int>> m_PendingDownloads;
QVector<DownloadInfo*> m_ActiveDownloads;
// Secondary index into m_ActiveDownloads keyed by m_DownloadID; kept in sync
// with every m_ActiveDownloads mutation.
QHash<DownloadID, DownloadInfo*> m_ByID;
QString m_OutputDirectory;
std::set<int> m_RequestIDs;
@@ -817,9 +706,13 @@ private:
SignalDownloadCallback m_DownloadFailed;
SignalDownloadCallback m_DownloadRemoved;
std::map<QString, int> m_DownloadFails;
bool m_ShowHidden;
MOBase::IPluginGame const* m_ManagedGame;
QTimer m_TimeoutTimer;
};
#endif // DOWNLOADMANAGER_H
+7 -13
View File
@@ -49,17 +49,12 @@ DownloadsTab::DownloadsTab(OrganizerCore& core, Ui::MainWindow* mwui)
SLOT(removeDownload(int, bool)));
connect(ui.list, SIGNAL(restoreDownload(int)), m_core.downloadManager(),
SLOT(restoreDownload(int)));
// The view reports actions by row; translate to DownloadID at the boundary.
connect(ui.list, &DownloadListView::cancelDownload, this, [this](int row) {
auto* dm = m_core.downloadManager();
dm->cancelDownload(dm->downloadIDAtRow(row));
});
connect(ui.list, &DownloadListView::pauseDownload, this, [this](int row) {
auto* dm = m_core.downloadManager();
dm->pauseDownload(dm->downloadIDAtRow(row));
});
connect(ui.list, &DownloadListView::resumeDownload, this, [this](int row) {
resumeDownload(row);
connect(ui.list, SIGNAL(cancelDownload(int)), m_core.downloadManager(),
SLOT(cancelDownload(int)));
connect(ui.list, SIGNAL(pauseDownload(int)), m_core.downloadManager(),
SLOT(pauseDownload(int)));
connect(ui.list, &DownloadListView::resumeDownload, [&](int i) {
resumeDownload(i);
});
}
@@ -110,7 +105,6 @@ void DownloadsTab::queryInfos()
void DownloadsTab::resumeDownload(int downloadIndex)
{
m_core.loggedInAction(ui.list, [this, downloadIndex] {
auto* dm = m_core.downloadManager();
dm->resumeDownload(dm->downloadIDAtRow(downloadIndex));
m_core.downloadManager()->resumeDownload(downloadIndex);
});
}
+24 -31
View File
@@ -480,23 +480,28 @@ bool InstallationManager::ensureValidModName(GuessedValue<QString>& name) const
return true;
}
InstallationResult InstallationManager::doInstall(ModInstallationInfo& info)
InstallationResult InstallationManager::doInstall(GuessedValue<QString>& modName,
QString gameName, int modID,
const QString& version,
const QString& newestVersion,
int categoryID, int fileCategoryID,
const QString& repository)
{
if (!ensureValidModName(info.modName)) {
if (!ensureValidModName(modName)) {
return {IPluginInstaller::RESULT_FAILED};
}
// determine target directory
InstallationResult result = testOverwrite(info.modName);
InstallationResult result = testOverwrite(modName);
if (!result) {
return result;
}
const bool merge = result.merged();
result.m_name = info.modName;
result.m_name = modName;
QString targetDirectory = QDir(m_ModsDirectory + "/" + info.modName).canonicalPath();
QString targetDirectory = QDir(m_ModsDirectory + "/" + modName).canonicalPath();
QString targetDirectoryNative = QDir::toNativeSeparators(targetDirectory);
log::debug("installing to \"{}\"", targetDirectoryNative);
@@ -527,31 +532,28 @@ InstallationResult InstallationManager::doInstall(ModInstallationInfo& info)
// overwrite settings only if they are actually are available or haven't been set
// before
if ((info.gameName != "") || !settingsFile.contains("gameName")) {
settingsFile.setValue("gameName", info.gameName);
if ((gameName != "") || !settingsFile.contains("gameName")) {
settingsFile.setValue("gameName", gameName);
}
if ((info.modID != 0) || !settingsFile.contains("modid")) {
settingsFile.setValue("modid", info.modID);
if ((modID != 0) || !settingsFile.contains("modid")) {
settingsFile.setValue("modid", modID);
}
if (!settingsFile.contains("version") ||
(!info.version.isEmpty() &&
(!merge || (VersionInfo(info.version) >=
(!version.isEmpty() &&
(!merge || (VersionInfo(version) >=
VersionInfo(settingsFile.value("version").toString()))))) {
settingsFile.setValue("version", info.version);
settingsFile.setValue("version", version);
}
if (!info.newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) {
settingsFile.setValue("newestVersion", info.newestVersion);
if (!newestVersion.isEmpty() || !settingsFile.contains("newestVersion")) {
settingsFile.setValue("newestVersion", newestVersion);
}
// issue #51 used to overwrite the manually set categories
if (!settingsFile.contains("category")) {
settingsFile.setValue("category", QString::number(info.categoryID));
settingsFile.setValue("category", QString::number(categoryID));
}
settingsFile.setValue("nexusFileStatus", info.fileCategoryID);
settingsFile.setValue("nexusFileStatus", fileCategoryID);
settingsFile.setValue("installationFile", m_CurrentFile);
settingsFile.setValue("repository", info.repository);
settingsFile.setValue("author", info.author);
settingsFile.setValue("uploader", info.uploader);
settingsFile.setValue("uploaderUrl", info.uploaderUrl);
settingsFile.setValue("repository", repository);
if (!merge) {
// this does not clear the list we have in memory but the mod is going to have to be
@@ -648,9 +650,6 @@ InstallationResult InstallationManager::install(const QString& fileName,
int categoryID = 0;
int fileCategoryID = 1;
QString repository = "Nexus";
QString author = "";
QString uploader = "";
QString uploaderUrl = "";
QString metaName = fileName + ".meta";
if (QFile(metaName).exists()) {
@@ -691,9 +690,6 @@ InstallationResult InstallationManager::install(const QString& fileName,
}
repository = metaFile.value("repository", "").toString();
fileCategoryID = metaFile.value("fileCategory", 1).toInt();
author = metaFile.value("author", "").toString();
uploader = metaFile.value("uploader", "").toString();
uploaderUrl = metaFile.value("uploaderUrl", "").toString();
}
if (version.isEmpty()) {
@@ -811,11 +807,8 @@ InstallationResult InstallationManager::install(const QString& fileName,
// the simple installer only prepares the installation, the rest
// works the same for all installers
ModInstallationInfo info{
modName, gameName, modID, version, newestVersion, categoryID,
fileCategoryID, repository, author, uploader, uploaderUrl};
installResult = doInstall(info);
installResult = doInstall(modName, gameName, modID, version, newestVersion,
categoryID, fileCategoryID, repository);
}
}
}

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