mirror of
https://github.com/ModOrganizer2/modorganizer.git
synced 2026-07-27 13:58:24 -07:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efe2a02d5d | ||
|
|
a931277b2b | ||
|
|
4907704797 | ||
|
|
c097670b09 | ||
|
|
a329ce59ac | ||
|
|
7a4bb3652e | ||
|
|
ab38cf0e05 | ||
|
|
0c95bf22e6 | ||
|
|
55de581ee9 | ||
|
|
6748953d35 | ||
|
|
22b1695701 | ||
|
|
3efaf7c946 | ||
|
|
41ffb25c03 | ||
|
|
9c6f48a440 | ||
|
|
3070a60ab7 | ||
|
|
6d02ef2b75 | ||
|
|
f5d89ad625 |
+4
-1
@@ -8,9 +8,12 @@ src/*.bak
|
||||
CMakeLists.txt.user
|
||||
edit
|
||||
/CMakeFiles
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/filetypes/
|
||||
!.idea/filetypes/qt-translations.xml
|
||||
/msbuild.log
|
||||
/*std*.log
|
||||
/*build
|
||||
|
||||
/src/version.aps
|
||||
.idea/
|
||||
|
||||
@@ -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.2
|
||||
rev: v22.1.5
|
||||
hooks:
|
||||
- id: clang-format
|
||||
'types_or': [c++, c]
|
||||
|
||||
@@ -3,9 +3,6 @@ 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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
[](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.
|
||||
@@ -20,14 +18,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/skyrimspecialedition/mods/6194)
|
||||
* on [NexusMods.com](https://www.nexusmods.com/site/mods/6)
|
||||
|
||||
## Old Download Location
|
||||
|
||||
|
||||
+5
-2
@@ -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)
|
||||
find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebSockets NetworkAuth)
|
||||
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 Version Dbghelp)
|
||||
Qt6::WebEngineWidgets Qt6::WebSockets Qt6::NetworkAuth Version Dbghelp)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/dlls.manifest.qt6"
|
||||
DESTINATION ${_bin}/dlls
|
||||
@@ -130,6 +130,9 @@ mo2_add_filter(NAME src/core GROUPS
|
||||
githubpp
|
||||
installationmanager
|
||||
nexusinterface
|
||||
nexusoauthlogin
|
||||
nexusoauthtokens
|
||||
nexusoauthconfig
|
||||
nxmaccessmanager
|
||||
organizercore
|
||||
game_features
|
||||
|
||||
+893
-876
File diff suppressed because it is too large
Load Diff
+15
-4
@@ -19,12 +19,17 @@ APIUserAccount::APIUserAccount() : m_type(APIUserAccountTypes::None) {}
|
||||
|
||||
bool APIUserAccount::isValid() const
|
||||
{
|
||||
return !m_key.isEmpty();
|
||||
return !m_accessToken.isEmpty() || !m_apiKey.isEmpty();
|
||||
}
|
||||
|
||||
const QString& APIUserAccount::accessToken() const
|
||||
{
|
||||
return m_accessToken;
|
||||
}
|
||||
|
||||
const QString& APIUserAccount::apiKey() const
|
||||
{
|
||||
return m_key;
|
||||
return m_apiKey;
|
||||
}
|
||||
|
||||
const QString& APIUserAccount::id() const
|
||||
@@ -47,9 +52,15 @@ const APILimits& APIUserAccount::limits() const
|
||||
return m_limits;
|
||||
}
|
||||
|
||||
APIUserAccount& APIUserAccount::apiKey(const QString& key)
|
||||
APIUserAccount& APIUserAccount::accessToken(const QString& token)
|
||||
{
|
||||
m_key = key;
|
||||
m_accessToken = token;
|
||||
return *this;
|
||||
}
|
||||
|
||||
APIUserAccount& APIUserAccount::apiKey(const QString& apiKey)
|
||||
{
|
||||
m_apiKey = apiKey;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
+14
-4
@@ -65,7 +65,12 @@ public:
|
||||
bool isValid() const;
|
||||
|
||||
/**
|
||||
* api key
|
||||
* OAuth access token
|
||||
*/
|
||||
const QString& accessToken() const;
|
||||
|
||||
/**
|
||||
* OAuth access token
|
||||
*/
|
||||
const QString& apiKey() const;
|
||||
|
||||
@@ -90,9 +95,14 @@ public:
|
||||
const APILimits& limits() const;
|
||||
|
||||
/**
|
||||
* sets the api key
|
||||
* sets the OAuth access token
|
||||
*/
|
||||
APIUserAccount& apiKey(const QString& key);
|
||||
APIUserAccount& accessToken(const QString& token);
|
||||
|
||||
/**
|
||||
* sets the OAuth access token
|
||||
*/
|
||||
APIUserAccount& apiKey(const QString& apiKey);
|
||||
|
||||
/**
|
||||
* sets the user id
|
||||
@@ -132,7 +142,7 @@ public:
|
||||
bool exhausted() const;
|
||||
|
||||
private:
|
||||
QString m_key, m_id, m_name;
|
||||
QString m_accessToken, m_apiKey, m_id, m_name;
|
||||
APIUserAccountTypes m_type;
|
||||
APILimits m_limits;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?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>
|
||||
+20
-14
@@ -57,13 +57,17 @@ void CategoryFactory::loadCategories()
|
||||
QFile categoryFile(categoriesFilePath());
|
||||
bool needLoad = false;
|
||||
|
||||
if (!categoryFile.open(QIODevice::ReadOnly)) {
|
||||
if (!categoryFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
needLoad = true;
|
||||
} else {
|
||||
int lineNum = 0;
|
||||
while (!categoryFile.atEnd()) {
|
||||
QByteArray line = categoryFile.readLine();
|
||||
++lineNum;
|
||||
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;
|
||||
}
|
||||
|
||||
QList<QByteArray> cells = line.split('|');
|
||||
if (cells.count() == 4) {
|
||||
std::vector<NexusCategory> nexusCats;
|
||||
@@ -76,7 +80,7 @@ void CategoryFactory::loadCategories()
|
||||
if (!ok) {
|
||||
log::error(tr("invalid category id {0}"), iter->constData());
|
||||
}
|
||||
nexusCats.push_back(NexusCategory("Unknown", temp));
|
||||
nexusCats.emplace_back("Unknown", temp);
|
||||
}
|
||||
}
|
||||
bool cell0Ok = true;
|
||||
@@ -103,16 +107,19 @@ void CategoryFactory::loadCategories()
|
||||
line.constData(), cells.count());
|
||||
}
|
||||
}
|
||||
categoryFile.close();
|
||||
|
||||
QFile nexusMapFile(nexusMappingFilePath());
|
||||
if (!nexusMapFile.open(QIODevice::ReadOnly)) {
|
||||
if (!nexusMapFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
needLoad = true;
|
||||
} else {
|
||||
int nexLineNum = 0;
|
||||
while (!nexusMapFile.atEnd()) {
|
||||
QByteArray nexLine = nexusMapFile.readLine();
|
||||
++nexLineNum;
|
||||
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;
|
||||
}
|
||||
|
||||
QList<QByteArray> nexCells = nexLine.split('|');
|
||||
if (nexCells.count() == 3) {
|
||||
std::vector<NexusCategory> nexusCats;
|
||||
@@ -129,12 +136,11 @@ 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)"), lineNum,
|
||||
log::error(tr("invalid nexus category line {0}: {1} ({2} cells)"), nexLineNum,
|
||||
nexLine.constData(), nexCells.count());
|
||||
}
|
||||
}
|
||||
}
|
||||
nexusMapFile.close();
|
||||
}
|
||||
std::sort(m_Categories.begin(), m_Categories.end());
|
||||
setParents();
|
||||
|
||||
+9
-2
@@ -857,7 +857,8 @@ po::options_description DownloadFileCommand::getVisibleOptions() const
|
||||
{
|
||||
po::options_description d;
|
||||
|
||||
d.add_options()("name,n", po::value<std::string>(), "(optional) the download name")(
|
||||
d.add_options()("game,g", po::value<std::string>(), "managed game")(
|
||||
"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");
|
||||
@@ -891,6 +892,7 @@ 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://")) {
|
||||
@@ -898,6 +900,10 @@ 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>());
|
||||
}
|
||||
@@ -917,7 +923,8 @@ 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, name, modName, version, source);
|
||||
core.downloadManager()->startDownloadURLWithMeta(url, game, name, modName, version,
|
||||
source);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -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 API key already exists) or explicitly (when
|
||||
// (NexusPage is skipped if the Nexus authorization already exists) or explicitly (when
|
||||
// only some info about the instance is missing on startup, such as a game
|
||||
// variant)
|
||||
//
|
||||
|
||||
@@ -103,6 +103,7 @@ void Page::next()
|
||||
|
||||
bool Page::action(CreateInstanceDialog::Actions a)
|
||||
{
|
||||
Q_UNUSED(a);
|
||||
// no-op
|
||||
return false;
|
||||
}
|
||||
@@ -1203,7 +1204,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::hasNexusApiKey();
|
||||
m_skip = GlobalSettings::hasNexusOAuthTokens() || GlobalSettings::hasNexusApiKey();
|
||||
}
|
||||
|
||||
NexusPage::~NexusPage() = default;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<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" />
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<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" />
|
||||
|
||||
+20
-13
@@ -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
|
||||
@@ -56,7 +58,9 @@ int DownloadList::columnCount(const QModelIndex&) const
|
||||
|
||||
QModelIndex DownloadList::index(int row, int column, const QModelIndex&) const
|
||||
{
|
||||
return createIndex(row, column, row);
|
||||
// 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));
|
||||
}
|
||||
|
||||
QModelIndex DownloadList::parent(const QModelIndex&) const
|
||||
@@ -110,14 +114,14 @@ QVariant DownloadList::data(const QModelIndex& index, int role) const
|
||||
bool pendingDownload = index.row() >= m_manager.numTotalDownloads();
|
||||
if (role == Qt::DisplayRole) {
|
||||
if (pendingDownload) {
|
||||
std::tuple<QString, int, int> nexusids =
|
||||
const DownloadManager::PendingDownload pending =
|
||||
m_manager.getPendingDownload(index.row() - m_manager.numTotalDownloads());
|
||||
switch (index.column()) {
|
||||
case COL_NAME:
|
||||
return tr("< game %1 mod %2 file %3 >")
|
||||
.arg(std::get<0>(nexusids))
|
||||
.arg(std::get<1>(nexusids))
|
||||
.arg(std::get<2>(nexusids));
|
||||
.arg(pending.gameName)
|
||||
.arg(pending.modID)
|
||||
.arg(pending.fileID);
|
||||
case COL_SIZE:
|
||||
return tr("Unknown");
|
||||
case COL_STATUS:
|
||||
@@ -239,16 +243,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
@@ -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;
|
||||
|
||||
+565
-469
File diff suppressed because it is too large
Load Diff
+248
-73
@@ -24,6 +24,7 @@ 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>
|
||||
@@ -31,7 +32,6 @@ 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,6 +40,7 @@ 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;
|
||||
|
||||
@@ -52,6 +53,60 @@ 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:
|
||||
void releaseSuspension();
|
||||
|
||||
QFileSystemWatcher m_watcher;
|
||||
int m_suspendDepth = 0;
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief manages downloading of files and provides progress information for gui
|
||||
*elements
|
||||
@@ -61,6 +116,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,
|
||||
@@ -79,6 +153,32 @@ 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
|
||||
{
|
||||
@@ -87,7 +187,7 @@ private:
|
||||
accumulator_set<qint64, stats<tag::rolling_mean>> m_DownloadTimeAcc;
|
||||
qint64 m_DownloadLast;
|
||||
qint64 m_DownloadTimeLast;
|
||||
unsigned int m_DownloadID;
|
||||
DownloadID m_DownloadID;
|
||||
QString m_FileName;
|
||||
QFile m_Output;
|
||||
QNetworkReply* m_Reply;
|
||||
@@ -116,8 +216,26 @@ 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);
|
||||
const QStringList& URLs,
|
||||
std::optional<DownloadID> reservedID = {});
|
||||
static DownloadInfo* createFromMeta(const QString& filePath, bool showHidden,
|
||||
const QString outputDirectory,
|
||||
std::optional<uint64_t> fileSize = {});
|
||||
@@ -132,14 +250,14 @@ private:
|
||||
**/
|
||||
void setName(QString newName, bool renameFile);
|
||||
|
||||
unsigned int downloadID() { return m_DownloadID; }
|
||||
DownloadID downloadID() { return m_DownloadID; }
|
||||
|
||||
bool isPausedState();
|
||||
|
||||
QString currentURL();
|
||||
|
||||
private:
|
||||
static unsigned int s_NextDownloadID;
|
||||
static DownloadID s_NextDownloadID;
|
||||
|
||||
private:
|
||||
DownloadInfo()
|
||||
@@ -191,19 +309,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
|
||||
**/
|
||||
@@ -239,18 +344,22 @@ 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<DownloadID> reservedID = {});
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* 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.
|
||||
* @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
|
||||
**/
|
||||
void addNXMDownload(const QString& url);
|
||||
DownloadID addNXMDownload(const QString& url);
|
||||
|
||||
/**
|
||||
* @brief retrieve the total number of downloads, both finished and unfinished
|
||||
@@ -271,9 +380,24 @@ public:
|
||||
* @brief retrieve the info of a pending download
|
||||
* @param index index of the pending download (index in the range [0,
|
||||
* numPendingDownloads()[)
|
||||
* @return pair of modid, fileid
|
||||
* @return the PendingDownload entry at the given index
|
||||
*/
|
||||
std::tuple<QString, int, int> getPendingDownload(int index);
|
||||
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;
|
||||
|
||||
/**
|
||||
* @brief retrieve the full path to the download specified by index
|
||||
@@ -408,11 +532,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 startDownloadURLWithMeta(const QString& url, const QString& game,
|
||||
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);
|
||||
|
||||
@@ -435,16 +565,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
|
||||
@@ -492,13 +644,13 @@ public slots:
|
||||
* @brief cancel the specified download. This will lead to the corresponding file to
|
||||
*be deleted
|
||||
*
|
||||
* @param index index of the download to cancel
|
||||
* @param id id of the download to cancel
|
||||
**/
|
||||
void cancelDownload(int index);
|
||||
void cancelDownload(DownloadID id);
|
||||
|
||||
void pauseDownload(int index);
|
||||
void pauseDownload(DownloadID id);
|
||||
|
||||
void resumeDownload(int index);
|
||||
void resumeDownload(DownloadID id);
|
||||
|
||||
void queryInfo(int index);
|
||||
|
||||
@@ -538,11 +690,23 @@ private slots:
|
||||
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
void downloadReadyRead();
|
||||
void downloadFinished(int index = 0);
|
||||
/**
|
||||
* @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 downloadError(QNetworkReply::NetworkError error);
|
||||
void metaDataChanged();
|
||||
void directoryChanged(const QString& dirctory);
|
||||
void checkDownloadTimeout();
|
||||
|
||||
private:
|
||||
void createMetaFile(DownloadInfo* info);
|
||||
@@ -561,8 +725,15 @@ public:
|
||||
QString getDownloadFileName(const QString& baseName, bool rename = false) const;
|
||||
|
||||
private:
|
||||
void startDownload(QNetworkReply* reply, DownloadInfo* newDownload, bool resume);
|
||||
void resumeDownloadInt(int index);
|
||||
/**
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @brief start a download from a url
|
||||
@@ -573,7 +744,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<DownloadID> reservedID = {});
|
||||
|
||||
// important: the caller has to lock the list-mutex, otherwise the
|
||||
// DownloadInfo-pointer might get invalidated at any time
|
||||
@@ -581,22 +753,40 @@ 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);
|
||||
|
||||
DownloadInfo* downloadInfoByID(unsigned int id);
|
||||
DownloadInfo* downloadInfoByID(DownloadID id);
|
||||
|
||||
QString displayNameByInfo(const DownloadInfo* info) const;
|
||||
|
||||
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);
|
||||
|
||||
QString getValidGameShortName(const QString& gameNexusName) const;
|
||||
|
||||
private:
|
||||
static const int AUTOMATIC_RETRIES = 3;
|
||||
|
||||
@@ -606,45 +796,30 @@ private:
|
||||
OrganizerCore* m_OrganizerCore;
|
||||
QWidget* m_ParentWidget;
|
||||
|
||||
QVector<std::tuple<QString, int, int>> m_PendingDownloads;
|
||||
QVector<PendingDownload> 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;
|
||||
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;
|
||||
|
||||
MOBase::IPluginGame const* m_ManagedGame;
|
||||
|
||||
QTimer m_TimeoutTimer;
|
||||
};
|
||||
|
||||
class ScopedDisableDirWatcher
|
||||
{
|
||||
public:
|
||||
ScopedDisableDirWatcher(DownloadManager* downloadManager);
|
||||
~ScopedDisableDirWatcher();
|
||||
|
||||
private:
|
||||
DownloadManager* m_downloadManager;
|
||||
};
|
||||
|
||||
#endif // DOWNLOADMANAGER_H
|
||||
|
||||
+13
-7
@@ -49,12 +49,17 @@ DownloadsTab::DownloadsTab(OrganizerCore& core, Ui::MainWindow* mwui)
|
||||
SLOT(removeDownload(int, bool)));
|
||||
connect(ui.list, SIGNAL(restoreDownload(int)), m_core.downloadManager(),
|
||||
SLOT(restoreDownload(int)));
|
||||
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);
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,6 +110,7 @@ void DownloadsTab::queryInfos()
|
||||
void DownloadsTab::resumeDownload(int downloadIndex)
|
||||
{
|
||||
m_core.loggedInAction(ui.list, [this, downloadIndex] {
|
||||
m_core.downloadManager()->resumeDownload(downloadIndex);
|
||||
auto* dm = m_core.downloadManager();
|
||||
dm->resumeDownload(dm->downloadIDAtRow(downloadIndex));
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user