summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/apiuseraccount.cpp33
-rw-r--r--src/apiuseraccount.h21
-rw-r--r--src/bbcode.cpp10
-rw-r--r--src/downloadlistwidget.cpp67
-rw-r--r--src/filedialogmemory.cpp4
-rw-r--r--src/logbuffer.cpp2
-rw-r--r--src/main.cpp11
-rw-r--r--src/mainwindow.cpp14
-rw-r--r--src/mainwindow.h2
-rw-r--r--src/mainwindow.ui2
-rw-r--r--src/modinfo.cpp2
-rw-r--r--src/modinfodialognexus.cpp56
-rw-r--r--src/nexusinterface.cpp25
-rw-r--r--src/nexusinterface.h1
-rw-r--r--src/nxmaccessmanager.cpp721
-rw-r--r--src/nxmaccessmanager.h170
-rw-r--r--src/organizer_en.ts770
-rw-r--r--src/organizercore.cpp3
-rw-r--r--src/overwriteinfodialog.cpp3
-rw-r--r--src/pch.h1
-rw-r--r--src/selfupdater.cpp2
-rw-r--r--src/settingsdialog.cpp246
-rw-r--r--src/settingsdialog.h34
-rw-r--r--src/settingsdialog.ui343
-rw-r--r--src/statusbar.cpp27
-rw-r--r--src/version.rc2
26 files changed, 1701 insertions, 871 deletions
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp
index b901e41a..35a868d5 100644
--- a/src/apiuseraccount.cpp
+++ b/src/apiuseraccount.cpp
@@ -1,10 +1,37 @@
#include "apiuseraccount.h"
+QString localizedUserAccountType(APIUserAccountTypes t)
+{
+ switch (t)
+ {
+ case APIUserAccountTypes::Regular:
+ return QObject::tr("Regular");
+
+ case APIUserAccountTypes::Premium:
+ return QObject::tr("Premium");
+
+ case APIUserAccountTypes::None: // fall-through
+ default:
+ return QObject::tr("None");
+ }
+}
+
+
APIUserAccount::APIUserAccount()
: m_type(APIUserAccountTypes::None)
{
}
+bool APIUserAccount::isValid() const
+{
+ return !m_key.isEmpty();
+}
+
+const QString& APIUserAccount::apiKey() const
+{
+ return m_key;
+}
+
const QString& APIUserAccount::id() const
{
return m_id;
@@ -25,6 +52,12 @@ const APILimits& APIUserAccount::limits() const
return m_limits;
}
+APIUserAccount& APIUserAccount::apiKey(const QString& key)
+{
+ m_key = key;
+ return *this;
+}
+
APIUserAccount& APIUserAccount::id(const QString& id)
{
m_id = id;
diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h
index 8a238d71..ea4e8685 100644
--- a/src/apiuseraccount.h
+++ b/src/apiuseraccount.h
@@ -18,6 +18,8 @@ enum class APIUserAccountTypes
Premium
};
+QString localizedUserAccountType(APIUserAccountTypes t);
+
/**
* current limits imposed on the user account
@@ -60,6 +62,17 @@ public:
APIUserAccount();
+
+ /**
+ * whether the user is logged in
+ */
+ bool isValid() const;
+
+ /**
+ * api key
+ */
+ const QString& apiKey() const;
+
/**
* user id
*/
@@ -82,6 +95,11 @@ public:
/**
+ * sets the api key
+ */
+ APIUserAccount& apiKey(const QString& key);
+
+ /**
* sets the user id
*/
APIUserAccount& id(const QString& id);
@@ -120,10 +138,9 @@ public:
bool exhausted() const;
private:
- QString m_id, m_name;
+ QString m_key, m_id, m_name;
APIUserAccountTypes m_type;
APILimits m_limits;
- APIStats m_stats;
};
#endif // APIUSERACCOUNT_H
diff --git a/src/bbcode.cpp b/src/bbcode.cpp
index 9f064106..323dd128 100644
--- a/src/bbcode.cpp
+++ b/src/bbcode.cpp
@@ -137,11 +137,13 @@ private:
m_TagMap["center"] = std::make_pair(QRegExp("\\[center\\](.*)\\[/center\\]"),
"<div align=\"center\">\\1</div>");
m_TagMap["quote"] = std::make_pair(QRegExp("\\[quote\\](.*)\\[/quote\\]"),
- "<blockquote>\"\\1\"</blockquote>");
+ "<figure class=\"quote\"><blockquote>\\1</blockquote></figure>");
m_TagMap["quote="] = std::make_pair(QRegExp("\\[quote=([^\\]]*)\\](.*)\\[/quote\\]"),
- "<blockquote>\"\\2\"<br/><span>--\\1</span></blockquote></p>");
+ "<figure class=\"quote\"><blockquote>\\2</blockquote></figure>");
+ m_TagMap["spoiler"] = std::make_pair(QRegExp("\\[spoiler\\](.*)\\[/spoiler\\]"),
+ "<details><summary>Spoiler: <div class=\"bbc_spoiler_show\">Show</div></summary><div class=\"spoiler_content\">\\1</div></details>");
m_TagMap["code"] = std::make_pair(QRegExp("\\[code\\](.*)\\[/code\\]"),
- "<pre>\\1</pre>");
+ "<code>\\1</code>");
m_TagMap["heading"]= std::make_pair(QRegExp("\\[heading\\](.*)\\[/heading\\]"),
"<h2><strong>\\1</strong></h2>");
m_TagMap["line"] = std::make_pair(QRegExp("\\[line\\]"),
@@ -181,7 +183,7 @@ private:
m_TagMap["email="] = std::make_pair(QRegExp("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"),
"<a href=\"mailto:\\1\">\\2</a>");
m_TagMap["youtube"] = std::make_pair(QRegExp("\\[youtube\\](.*)\\[/youtube\\]"),
- "<a href=\"http://www.youtube.com/v/\\1\">http://www.youtube.com/v/\\1</a>");
+ "<a href=\"https://www.youtube.com/watch?v=\\1\">https://www.youtube.com/watch?v=\\1</a>");
// make all patterns non-greedy and case-insensitive
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index c75ecdd2..e2a6f321 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -201,40 +201,49 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point)
QModelIndex index = indexAt(point);
bool hidden = false;
- if (index.row() >= 0) {
- m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
- DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- hidden = m_Manager->isHidden(m_ContextRow);
+ try
+ {
+ if (index.row() >= 0) {
+ m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
+ DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextRow))
- menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
- else
- menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
- menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ hidden = m_Manager->isHidden(m_ContextRow);
- menu.addSeparator();
+ if (state >= DownloadManager::STATE_READY) {
+ menu.addAction(tr("Install"), this, SLOT(issueInstall()));
+ if (m_Manager->isInfoIncomplete(m_ContextRow))
+ menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
+ else
+ menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
+ menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- if (hidden)
- menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
- else
- menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
- } else if (state == DownloadManager::STATE_DOWNLOADING) {
- menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
- menu.addAction(tr("Pause"), this, SLOT(issuePause()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
- || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- menu.addAction(tr("Resume"), this, SLOT(issueResume()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- }
+ menu.addSeparator();
- menu.addSeparator();
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ if (hidden)
+ menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
+ else
+ menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
+ } else if (state == DownloadManager::STATE_DOWNLOADING) {
+ menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
+ menu.addAction(tr("Pause"), this, SLOT(issuePause()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
+ || (state == DownloadManager::STATE_PAUSING)) {
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ menu.addAction(tr("Resume"), this, SLOT(issueResume()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ }
+
+ menu.addSeparator();
+ }
+ } catch(std::exception&)
+ {
+ // this happens when the download index is not found, ignore it and don't
+ // display download-specific actions
}
+
menu.addAction(tr("Delete Installed Downloads..."), this, SLOT(issueDeleteCompleted()));
menu.addAction(tr("Delete Uninstalled Downloads..."), this, SLOT(issueDeleteUninstalled()));
menu.addAction(tr("Delete All Downloads..."), this, SLOT(issueDeleteAll()));
diff --git a/src/filedialogmemory.cpp b/src/filedialogmemory.cpp
index 554a6235..0e3e9793 100644
--- a/src/filedialogmemory.cpp
+++ b/src/filedialogmemory.cpp
@@ -66,7 +66,7 @@ QString FileDialogMemory::getOpenFileName(
if (currentDir.isEmpty()) {
auto itor = instance().m_Cache.find(dirID);
if (itor != instance().m_Cache.end()) {
- currentDir = itor->first;
+ currentDir = itor->second;
}
}
@@ -90,7 +90,7 @@ QString FileDialogMemory::getExistingDirectory(
if (currentDir.isEmpty()) {
auto itor = instance().m_Cache.find(dirID);
if (itor != instance().m_Cache.end()) {
- currentDir = itor->first;
+ currentDir = itor->second;
}
}
diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp
index 7314624f..dfe8f943 100644
--- a/src/logbuffer.cpp
+++ b/src/logbuffer.cpp
@@ -191,7 +191,7 @@ QVariant LogBuffer::data(const QModelIndex &index, int role) const
switch (role) {
case Qt::DisplayRole: {
if (index.column() == 0) {
- return m_Messages[msgIndex].time;
+ return m_Messages[msgIndex].time.toString("H: mm: ss");
} else if (index.column() == 1) {
const QString &msg = m_Messages[msgIndex].message;
if (msg.length() < 200) {
diff --git a/src/main.cpp b/src/main.cpp
index ec60b605..bf4e5b97 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -700,6 +700,9 @@ int runApplication(MOApplication &application, SingleInstance &instance,
// set up main window and its data structures
MainWindow mainWindow(settings, organizer, pluginContainer);
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(&mainWindow);
+
QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
SLOT(setStyleFile(QString)));
QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
@@ -713,7 +716,13 @@ int runApplication(MOApplication &application, SingleInstance &instance,
mainWindow.activateWindow();
splash.finish(&mainWindow);
- return application.exec();
+
+ const auto ret = application.exec();
+
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(nullptr);
+
+ return ret;
}
} catch (const std::exception &e) {
reportError(e.what());
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 8a101be7..d06f79f2 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -398,7 +398,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi()));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString)));
connect(
NexusInterface::instance(&pluginContainer)->getAccessManager(),
@@ -2569,8 +2568,14 @@ void MainWindow::directory_refreshed()
{
// some problem-reports may rely on the virtual directory tree so they need to be updated
// now
- refreshDataTreeKeepExpandedNodes();
updateProblemsButton();
+
+
+ //Some better check for the current tab is needed.
+ if (ui->tabWidget->currentIndex() == 2) {
+ refreshDataTreeKeepExpandedNodes();
+ }
+
}
void MainWindow::esplist_changed()
@@ -3095,11 +3100,6 @@ void MainWindow::untrack_clicked()
});
}
-void MainWindow::validationFailed(const QString &error)
-{
- qDebug("Nexus API validation failed: %s", qUtf8Printable(error));
-}
-
void MainWindow::windowTutorialFinished(const QString &windowName)
{
m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true);
diff --git a/src/mainwindow.h b/src/mainwindow.h
index eee269cf..80508787 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -510,8 +510,6 @@ private slots:
// nexus related
void checkModsForUpdates();
- void validationFailed(const QString &message);
-
void linkClicked(const QString &url);
void updateAvailable();
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 6a816262..70d1cf39 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -1733,7 +1733,7 @@ p, li { white-space: pre-wrap; }
<bool>true</bool>
</property>
<property name="text">
- <string>St&amp;atus bar</string>
+ <string>Status &amp;bar</string>
</property>
</action>
</widget>
diff --git a/src/modinfo.cpp b/src/modinfo.cpp
index 73ead71c..3484b644 100644
--- a/src/modinfo.cpp
+++ b/src/modinfo.cpp
@@ -323,7 +323,7 @@ bool ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *recei
updatesAvailable = false;
} else {
qInfo() << tr(
- "You have mods that haven't been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. "
+ "You have mods that haven't been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. "
"This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods."
);
}
diff --git a/src/modinfodialognexus.cpp b/src/modinfodialognexus.cpp
index 04683c89..e606525c 100644
--- a/src/modinfodialognexus.cpp
+++ b/src/modinfodialognexus.cpp
@@ -210,10 +210,66 @@ void NexusTab::onModChanged()
max-width: 1060px;
margin-left: auto;
margin-right: auto;
+ padding-right: 7px;
+ padding-left: 7px;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ }
+
+ img {
+ max-width: 100%;
+ }
+
+ figure.quote {
+ position: relative;
+ padding: 24px;
+ margin: 10px 20px 10px 10px;
+ color: #e1e1e1;
+ line-height: 1.5;
+ font-style: italic;
+ border-left: 6px solid #57a5cc;
+ border-left-color: rgb(87, 165, 204);
+ background: #383838 url(data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbi1xdW90ZSIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJmaWxsOnJnYig2OSwgNjksIDcwKTtoZWlnaHQ6MjlweDtsZWZ0OjE1cHg7cG9zaXRpb246YWJzb2x1dGU7dG9wOjE1cHg7d2lkdGg6MzhweDsiPjxwYXRoIGNsYXNzPSJwYXRoMSIgZD0iTTAgMjAuNjc0YzAgNy4yMjUgNC42NjggMTEuMzM3IDkuODkyIDExLjMzNyA0LjgyNC0wLjA2MiA4LjcxOS0zLjk1NiA4Ljc4MS04Ljc3NSAwLTQuNzg1LTMuMzM0LTguMDA5LTcuNTU4LTguMDA5LTAuMDc4LTAuMDA0LTAuMTctMC4wMDYtMC4yNjItMC4wMDYtMC43MDMgMC0xLjM3NyAwLjEyNC0yLjAwMSAwLjM1MiAxLjA0MS00LjAxNCA1LjE1My04LjY4MyA4LjcxLTEwLjU3MmwtNi4xMTMtNS4wMDJjLTYuODkxIDQuODkxLTExLjQ0OCAxMi4zMzgtMTEuNDQ4IDIwLjY3NHpNMjIuNjc1IDIwLjY3NGMwIDcuMjI1IDQuNjY4IDExLjMzNyA5Ljg5MiAxMS4zMzcgNC44LTAuMDU2IDguNjctMy45NjEgOC42Ny04Ljc2OSAwLTAuMDA0IDAtMC4wMDggMC0wLjAxMiAwLTQuNzc5LTMuMjIzLTguMDAyLTcuNDQ3LTguMDAyLTAuMDk1LTAuMDA2LTAuMjA2LTAuMDA5LTAuMzE4LTAuMDA5LTAuNjg0IDAtMS4zMzkgMC4xMjYtMS45NDMgMC4zNTUgMC45MjctNC4wMTQgNS4xNS04LjY4MiA4LjcwNy0xMC41NzJsLTYuMTI0LTUuMDAyYy02Ljg5MSA0Ljg5MS0xMS40MzcgMTIuMzM4LTExLjQzNyAyMC42NzR6IiBzdHlsZT0iZmlsbDpyZ2IoNjksIDY5LCA3MCk7aGVpZ2h0OmF1dG87d2lkdGg6YXV0bzsiLz48L3N2Zz4=) no-repeat;
+ }
+
+ figure.quote blockquote {
+ position: relative;
+ margin: 0;
+ padding: 0;
+ }
+
+ div.spoiler_content {
+ background: #262626;
+ border: 1px dashed #3b3b3b;
+ padding: 5px;
+ margin: 5px;
+ }
+
+ div.bbc_spoiler_show{
+ border: 1px solid black;
+ background-color: #454545;
+ font-size: 11px;
+ padding: 3px;
+ color: #E6E6E6;
+ border-radius: 3px;
+ display: inline-block;
+ cursor: pointer;
+ }
+
+ details summary::-webkit-details-marker {
+ display:none;
+ }
+
+ summary:focus {
+ outline: 0;
}
a
{
+ /*should avoid overflow with long links forcing wordwrap regardless of spaces*/
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+
color: #8197ec;
text-decoration: none;
}
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index ee9acf2c..2bcd72f3 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -208,12 +208,27 @@ APILimits NexusInterface::defaultAPILimits()
APILimits NexusInterface::parseLimits(const QNetworkReply* reply)
{
+ return parseLimits(reply->rawHeaderPairs());
+}
+
+APILimits NexusInterface::parseLimits(
+ const QList<QNetworkReply::RawHeaderPair>& headers)
+{
APILimits limits;
- limits.maxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt();
- limits.remainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt();
- limits.maxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt();
- limits.remainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt();
+ for (const auto& pair : headers) {
+ const auto name = QString(pair.first).toLower();
+
+ if (name == "x-rl-daily-limit") {
+ limits.maxDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-daily-remaining") {
+ limits.remainingDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-limit") {
+ limits.maxHourlyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-remaining") {
+ limits.remainingHourlyRequests = pair.second.toInt();
+ }
+ }
return limits;
}
@@ -765,7 +780,7 @@ void NexusInterface::nextRequest()
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
- request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8());
+ request.setRawHeader("APIKEY", m_User.apiKey().toUtf8());
request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Protocol-Version", "1.0.0");
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index 6e768149..0b1763c4 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -151,6 +151,7 @@ public:
public:
static APILimits defaultAPILimits();
static APILimits parseLimits(const QNetworkReply* reply);
+ static APILimits parseLimits(const QList<QNetworkReply::RawHeaderPair>& headers);
~NexusInterface();
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index ee6c03f1..c413e156 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -40,21 +40,530 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QJsonArray>
using namespace MOBase;
+using namespace std::chrono_literals;
-namespace {
- QString const nexusBaseUrl("https://api.nexusmods.com/v1");
+const QString NexusBaseUrl("https://api.nexusmods.com/v1");
+const std::chrono::seconds NXMAccessManager::ValidationTimeout = 10s;
+const QString NexusSSO("wss://sso.nexusmods.com");
+const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2");
+
+
+ValidationProgressDialog::ValidationProgressDialog(std::chrono::seconds t)
+ : m_timeout(t), m_bar(nullptr), m_buttons(nullptr), m_timer(nullptr)
+{
+ m_bar = new QProgressBar;
+ m_bar->setTextVisible(false);
+
+ auto* label = new QLabel(tr("Validating Nexus Connection"));
+ label->setAlignment(Qt::AlignHCenter);
+
+ auto* vbox = new QVBoxLayout(this);
+ vbox->addWidget(label);
+ vbox->addWidget(m_bar);
+
+ m_buttons = new QDialogButtonBox;
+ m_buttons->addButton(tr("Hide"), QDialogButtonBox::RejectRole);
+ connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); });
+ vbox->addWidget(m_buttons);
+}
+
+void ValidationProgressDialog::setParentWidget(QWidget* w)
+{
+ const auto wasVisible = isVisible();
+
+ hide();
+ setParent(w, windowFlags() | Qt::Dialog);
+ setModal(false);
+ setVisible(wasVisible);
+}
+
+void ValidationProgressDialog::start()
+{
+ if (!m_timer) {
+ m_timer = new QTimer(this);
+ connect(m_timer, &QTimer::timeout, [&]{ onTimer(); });
+ m_timer->setInterval(100ms);
+ }
+
+ m_bar->setRange(0, m_timeout.count());
+ m_bar->setValue(0);
+
+ m_elapsed.start();
+ m_timer->start();
+
+ show();
+}
+
+void ValidationProgressDialog::stop()
+{
+ if (m_timer) {
+ m_timer->stop();
+ }
+
+ hide();
+}
+
+void ValidationProgressDialog::closeEvent(QCloseEvent* e)
+{
+ hide();
+ e->ignore();
+}
+
+void ValidationProgressDialog::onButton(QAbstractButton* b)
+{
+ if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) {
+ hide();
+ } else {
+ qCritical() << "validation dialog: unknown button pressed";
+ }
+}
+
+void ValidationProgressDialog::onTimer()
+{
+ m_bar->setValue(m_elapsed.elapsed() / 1000);
+}
+
+
+NexusSSOLogin::NexusSSOLogin()
+ : m_keyReceived(false), m_active(false)
+{
+ m_timeout.setInterval(NXMAccessManager::ValidationTimeout);
+ m_timeout.setSingleShot(true);
+
+ QObject::connect(
+ &m_socket, &QWebSocket::connected,
+ [&]{ onConnected(); });
+
+ QObject::connect(
+ &m_socket, qOverload<QAbstractSocket::SocketError>(&QWebSocket::error),
+ [&](auto&& e){ onError(e); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::textMessageReceived,
+ [&](auto&& s){ onMessage(s); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::disconnected,
+ [&]{ onDisconnected(); });
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+QString NexusSSOLogin::stateToString(States s, const QString& e)
+{
+ switch (s)
+ {
+ case ConnectingToSSO:
+ return QObject::tr("Connecting to Nexus...");
+
+ case WaitingForToken:
+ return QObject::tr("Waiting for Nexus...");
+
+ case WaitingForBrowser:
+ return QObject::tr(
+ "Opened Nexus in browser.\n"
+ "Switch to your browser and accept the request.");
+
+ case Finished:
+ return QObject::tr("Finished.");
+
+ case Timeout:
+ return QObject::tr(
+ "No answer from Nexus.\n"
+ "A firewall might be blocking Mod Organizer.");
+
+ case ClosedByRemote:
+ return QObject::tr("Nexus closed the connection.");
+
+ case Cancelled:
+ return QObject::tr("Cancelled.");
+
+ case Error: // fall-through
+ default:
+ {
+ if (e.isEmpty()) {
+ return QString("%1").arg(s);
+ } else {
+ return e;
+ }
+ }
+ }
+}
+
+void NexusSSOLogin::start()
+{
+ m_active = true;
+ setState(ConnectingToSSO);
+ m_timeout.start();
+ m_socket.open(NexusSSO);
+}
+
+void NexusSSOLogin::cancel()
+{
+ if (m_active) {
+ abort();
+ setState(Cancelled);
+ }
+}
+
+void NexusSSOLogin::close()
+{
+ if (m_active) {
+ m_active = false;
+ m_timeout.stop();
+ m_socket.close();
+ }
+}
+
+void NexusSSOLogin::abort()
+{
+ m_active = false;
+ m_timeout.stop();
+ m_socket.abort();
+}
+
+bool NexusSSOLogin::isActive() const
+{
+ return m_active;
+}
+
+void NexusSSOLogin::setState(States s, const QString& error)
+{
+ if (stateChanged) {
+ stateChanged(s, error);
+ }
+}
+
+void NexusSSOLogin::onConnected()
+{
+ setState(WaitingForToken);
+
+ m_keyReceived = false;
+
+ boost::uuids::random_generator generator;
+ boost::uuids::uuid sessionId = generator();
+ m_guid = boost::uuids::to_string(sessionId).c_str();
+
+ QJsonObject data;
+ data.insert(QString("id"), QJsonValue(m_guid));
+ data.insert(QString("protocol"), 2);
+
+ const QString message = QJsonDocument(data).toJson();
+ m_socket.sendTextMessage(message);
+}
+
+void NexusSSOLogin::onMessage(const QString& s)
+{
+ const QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
+ const QVariantMap root = doc.object().toVariantMap();
+
+ if (!root["success"].toBool()) {
+ close();
+
+ setState(Error, QString("There was a problem with SSO initialization: %1")
+ .arg(root["error"].toString()));
+
+ return;
+ }
+
+ const QVariantMap data = root["data"].toMap();
+
+ if (data.contains("connection_token")) {
+ // first answer
+
+ // open browser
+ const auto url = NexusSSOPage.arg(m_guid);
+ shell::OpenLink(url);
+
+ m_timeout.stop();
+ setState(WaitingForBrowser);
+ } else {
+ // second answer
+ const auto key = data["api_key"].toString();
+ close();
+
+ if (keyChanged) {
+ keyChanged(key);
+ }
+
+ setState(Finished);
+ }
+}
+
+void NexusSSOLogin::onDisconnected()
+{
+ if (m_active) {
+ m_active = false;
+
+ if (!m_keyReceived) {
+ setState(ClosedByRemote);
+ }
+ }
+}
+
+void NexusSSOLogin::onError(QAbstractSocket::SocketError e)
+{
+ if (m_active) {
+ setState(Error, m_socket.errorString());
+ close();
+ }
+}
+
+void NexusSSOLogin::onSslErrors(const QList<QSslError>& errors)
+{
+ if (m_active) {
+ for (const auto& e : errors) {
+ setState(Error, e.errorString());
+ }
+ }
+}
+
+void NexusSSOLogin::onTimeout()
+{
+ abort();
+ setState(Timeout);
+}
+
+
+NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am)
+ : m_manager(am), m_reply(nullptr), m_active(false)
+{
+ m_timeout.setInterval(NXMAccessManager::ValidationTimeout);
+ m_timeout.setSingleShot(true);
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+NexusKeyValidator::~NexusKeyValidator()
+{
+ abort();
+}
+
+QString NexusKeyValidator::stateToString(States s, const QString& e)
+{
+ switch (s)
+ {
+ case NexusKeyValidator::Connecting:
+ return QObject::tr("Connecting to Nexus...");
+
+ case NexusKeyValidator::Finished:
+ return QObject::tr("Finished.");
+
+ case NexusKeyValidator::InvalidJson:
+ return QObject::tr("Invalid JSON");
+
+ case NexusKeyValidator::BadResponse:
+ return QObject::tr("Bad response");
+
+ case NexusKeyValidator::Timeout:
+ return QObject::tr("There was a timeout during the request");
+
+ case NexusKeyValidator::Cancelled:
+ return QObject::tr("Cancelled");
+
+ case NexusKeyValidator::Error: // fall-through
+ default:
+ {
+ if (e.isEmpty()) {
+ return QString("%1").arg(s);
+ } else {
+ return e;
+ }
+ }
+ }
}
+void NexusKeyValidator::start(const QString& key)
+{
+ if (m_reply) {
+ abort();
+ return;
+ }
+
+ m_active = true;
+ setState(Connecting);
+
+ const QString requestUrl(NexusBaseUrl + "/users/validate");
+ QNetworkRequest request(requestUrl);
+
+ request.setRawHeader("APIKEY", key.toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_manager.userAgent().toUtf8());
+ request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
+ request.setRawHeader("Protocol-Version", "1.0.0");
+ request.setRawHeader("Application-Name", "MO2");
+ request.setRawHeader("Application-Version", m_manager.MOVersion().toUtf8());
+
+ m_reply = m_manager.get(request);
+ if (!m_reply) {
+ close();
+ setState(Error, QObject::tr("Failed to request %1").arg(requestUrl));
+ return;
+ }
+
+ m_timeout.start(NXMAccessManager::ValidationTimeout);
+
+ QObject::connect(
+ m_reply, &QNetworkReply::finished,
+ [&]{ onFinished(); });
+
+ QObject::connect(
+ m_reply, &QNetworkReply::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+}
+
+void NexusKeyValidator::cancel()
+{
+ if (m_active) {
+ abort();
+ setState(Cancelled);
+ }
+}
+
+bool NexusKeyValidator::isActive() const
+{
+ return m_active;
+}
+
+void NexusKeyValidator::close()
+{
+ m_active = false;
+ m_timeout.stop();
+
+ if (m_reply) {
+ m_reply->disconnect();
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+
+void NexusKeyValidator::abort()
+{
+ m_active = false;
+ m_timeout.stop();
+
+ if (m_reply) {
+ m_reply->disconnect();
+ m_reply->abort();
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+
+void NexusKeyValidator::setState(States s, const QString& error)
+{
+ if (stateChanged) {
+ stateChanged(s, error);
+ }
+}
+
+void NexusKeyValidator::onFinished()
+{
+ if (!m_reply) {
+ // shouldn't happen
+ return;
+ }
+
+ m_timeout.stop();
+
+ const auto code = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ const auto doc = QJsonDocument::fromJson(m_reply->readAll());
+ const auto headers = m_reply->rawHeaderPairs();
+ const auto error = m_reply->errorString();
+
+ close();
+
+ const QJsonObject data = doc.object();
+
+ if (code != 200) {
+ handleError(code, data.value("message").toString(), error);
+ return;
+ }
+
+ if (doc.isNull()) {
+ setState(InvalidJson);
+ return;
+ }
+
+ if (!data.contains("user_id")) {
+ setState(BadResponse);
+ return;
+ }
+
+ const int id = data.value("user_id").toInt();
+ const QString key = data.value("key").toString();
+ const QString name = data.value("name").toString();
+ const bool premium = data.value("is_premium").toBool();
+
+ const auto user = APIUserAccount()
+ .apiKey(key)
+ .id(QString("%1").arg(id))
+ .name(name)
+ .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
+ .limits(NexusInterface::parseLimits(headers));
+
+ if (finished) {
+ setState(Finished);
+ finished(user);
+ }
+}
+
+void NexusKeyValidator::onSslErrors(const QList<QSslError>& errors)
+{
+ if (m_active) {
+ for (const auto& e : errors) {
+ setState(Error, e.errorString());
+ }
+ }
+}
+
+void NexusKeyValidator::onTimeout()
+{
+ abort();
+ setState(Timeout);
+}
+
+void NexusKeyValidator::handleError(
+ int code, const QString& nexusMessage, const QString& httpError)
+{
+ QString s = httpError;
+
+ if (!nexusMessage.isEmpty()) {
+ if (!s.isEmpty()) {
+ s += ", ";
+ }
+
+ s += nexusMessage;
+ }
+
+ if (code != 0) {
+ if (s.isEmpty()) {
+ s = QString("HTTP code %1").arg(code);
+ } else {
+ s += QString(" (%1)").arg(code);
+ }
+ }
+
+ setState(Error, s);
+}
+
+
+
NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
: QNetworkAccessManager(parent)
- , m_ValidateReply(nullptr)
+ , m_ProgressDialog(new ValidationProgressDialog(ValidationTimeout))
, m_MOVersion(moVersion)
+ , m_validator(*this)
+ , m_validationState(NotChecked)
{
- m_ValidateTimeout.setSingleShot(true);
- m_ValidateTimeout.setInterval(30000);
- connect(&m_ValidateTimeout, SIGNAL(timeout()), this, SLOT(validateTimeout()));
- setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
+ m_validator.stateChanged = [&](auto&& s, auto&& e){ onValidatorState(s, e); };
+ m_validator.finished = [&](auto&& user){ onValidatorFinished(user); };
+
+ setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators(
+ Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) {
// why is this necessary all of a sudden?
@@ -62,12 +571,9 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
}
}
-NXMAccessManager::~NXMAccessManager()
+void NXMAccessManager::setTopLevelWidget(QWidget* w)
{
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- }
+ m_ProgressDialog->setParentWidget(w);
}
QNetworkReply *NXMAccessManager::createRequest(
@@ -90,10 +596,9 @@ QNetworkReply *NXMAccessManager::createRequest(
}
}
-
void NXMAccessManager::showCookies() const
{
- QUrl url(nexusBaseUrl + "/");
+ QUrl url(NexusBaseUrl + "/");
for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) {
qDebug("%s - %s (expires: %s)",
cookie.name().constData(), cookie.value().constData(),
@@ -111,90 +616,81 @@ void NXMAccessManager::clearCookies()
}
}
-void NXMAccessManager::startValidationCheck()
+void NXMAccessManager::startValidationCheck(const QString& key)
{
- qDebug("Checking Nexus API Key...");
- QString requestString = nexusBaseUrl + "/users/validate";
-
- QNetworkRequest request(requestString);
- request.setRawHeader("APIKEY", m_ApiKey.toUtf8());
- request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, userAgent().toUtf8());
- request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
- request.setRawHeader("Protocol-Version", "1.0.0");
- request.setRawHeader("Application-Name", "MO2");
- request.setRawHeader("Application-Version", m_MOVersion.toUtf8());
+ m_validationState = NotChecked;
+ m_validator.start(key);
+ m_ProgressDialog->start();
+}
- m_ProgressDialog = new QProgressDialog(nullptr);
- m_ProgressDialog->setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- m_ProgressDialog->show();
- QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback
+void NXMAccessManager::onValidatorState(
+ NexusKeyValidator::States s, const QString& e)
+{
+ if (s == NexusKeyValidator::Connecting || s == NexusKeyValidator::Finished) {
+ // no-op, success is handled in onValidatorFinished()
+ return;
+ }
- m_ValidateReply = get(request);
- m_ValidateTimeout.start();
- m_ValidateState = VALIDATE_CHECKING;
- connect(m_ValidateReply, SIGNAL(finished()), this, SLOT(validateFinished()));
- connect(m_ValidateReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(validateError(QNetworkReply::NetworkError)));
+ m_ProgressDialog->stop();
+ m_validationState = Invalid;
+ emit validateFailed(NexusKeyValidator::stateToString(s, e));
}
+void NXMAccessManager::onValidatorFinished(const APIUserAccount& user)
+{
+ m_ProgressDialog->stop();
+
+ m_validationState = Valid;
+ emit credentialsReceived(user);
+ emit validateSuccessful(true);
+}
bool NXMAccessManager::validated() const
{
- if (m_ValidateState == VALIDATE_CHECKING) {
- QProgressDialog progress;
- progress.setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- progress.show();
- while (m_ValidateState == VALIDATE_CHECKING) {
- QCoreApplication::processEvents();
- QThread::msleep(100);
- }
- progress.hide();
+ if (m_validator.isActive()) {
+ m_ProgressDialog->show();
}
- return m_ValidateState == VALIDATE_VALID;
+ return (m_validationState == Valid);
}
-
void NXMAccessManager::refuseValidation()
{
- m_ValidateState = VALIDATE_REFUSED;
+ m_validationState = Invalid;
}
-
bool NXMAccessManager::validateAttempted() const
{
- return m_ValidateState != VALIDATE_NOT_CHECKED;
+ return (m_validationState != NotChecked);
}
-
bool NXMAccessManager::validateWaiting() const
{
- return m_ValidateReply != nullptr;
+ return m_validator.isActive();
}
-
void NXMAccessManager::apiCheck(const QString &apiKey, bool force)
{
- if (m_ValidateReply != nullptr) {
+ if (m_validator.isActive()) {
return;
}
if (force) {
- m_ValidateState = VALIDATE_NOT_CHECKED;
+ m_validationState = NotChecked;
}
- if (m_ValidateState == VALIDATE_VALID) {
+ if (m_validationState == Valid) {
emit validateSuccessful(false);
return;
}
- m_ApiKey = apiKey;
- startValidationCheck();
+ startValidationCheck(apiKey);
}
+const QString& NXMAccessManager::MOVersion() const
+{
+ return m_MOVersion;
+}
QString NXMAccessManager::userAgent(const QString &subModule) const
{
@@ -213,109 +709,8 @@ QString NXMAccessManager::userAgent(const QString &subModule) const
return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion());
}
-
-QString NXMAccessManager::apiKey() const
-{
- return m_ApiKey;
-}
-
void NXMAccessManager::clearApiKey()
{
- m_ApiKey = "";
- m_ValidateState = VALIDATE_NOT_VALID;
-
+ m_validator.cancel();
emit credentialsReceived(APIUserAccount());
}
-
-void NXMAccessManager::validateTimeout()
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
-
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- }
-
- emit validateFailed(tr("There was a timeout during the request"));
-}
-
-
-void NXMAccessManager::validateError(QNetworkReply::NetworkError)
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
-
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->disconnect();
- QString error = m_ValidateReply->errorString();
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- emit validateFailed(error);
- } else {
- emit validateFailed(tr("Unknown error"));
- }
-}
-
-
-void NXMAccessManager::validateFinished()
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
-
- if (m_ValidateReply != nullptr) {
- QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll());
- if (!jdoc.isNull()) {
- QJsonObject credentialsData = jdoc.object();
- if (credentialsData.contains("user_id")) {
- int id = credentialsData.value("user_id").toInt();
- QString name = credentialsData.value("name").toString();
- bool premium = credentialsData.value("is_premium").toBool();
-
- const auto user = APIUserAccount()
- .id(QString("%1").arg(id))
- .name(name)
- .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
- .limits(NexusInterface::parseLimits(m_ValidateReply));
-
-
- emit credentialsReceived(user);
-
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
-
- m_ValidateState = VALIDATE_VALID;
- emit validateSuccessful(true);
-
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
- emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString()));
- }
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Could not parse response. Invalid JSON."));
- }
- }
- else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Unknown error."));
- }
-}
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index 1bdeae40..eed7c1c9 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -25,9 +25,137 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QTimer>
#include <QNetworkReply>
#include <QProgressDialog>
+#include <QElapsedTimer>
+#include <QDialogButtonBox>
+#include <QWebSocket>
#include <set>
namespace MOBase { class IPluginGame; }
+class NXMAccessManager;
+
+class ValidationProgressDialog : private QDialog
+{
+ Q_OBJECT;
+
+public:
+ ValidationProgressDialog(std::chrono::seconds timeout);
+
+ void setParentWidget(QWidget* w);
+
+ void start();
+ void stop();
+
+ using QDialog::show;
+
+protected:
+ void closeEvent(QCloseEvent* e) override;
+
+private:
+ std::chrono::seconds m_timeout;
+ QProgressBar* m_bar;
+ QDialogButtonBox* m_buttons;
+ QTimer* m_timer;
+ QElapsedTimer m_elapsed;
+
+ void onButton(QAbstractButton* b);
+ void onTimer();
+};
+
+
+class NexusSSOLogin
+{
+public:
+ enum States
+ {
+ ConnectingToSSO,
+ WaitingForToken,
+ WaitingForBrowser,
+ Finished,
+ Timeout,
+ ClosedByRemote,
+ Cancelled,
+ Error
+ };
+
+ std::function<void (QString)> keyChanged;
+ std::function<void (States, QString)> stateChanged;
+
+ static QString stateToString(States s, const QString& e);
+
+ NexusSSOLogin();
+
+ void start();
+ void cancel();
+
+ bool isActive() const;
+
+private:
+ QWebSocket m_socket;
+ QString m_guid;
+ bool m_keyReceived;
+ bool m_active;
+ QTimer m_timeout;
+
+ void setState(States s, const QString& error={});
+
+ void close();
+ void abort();
+
+ void onConnected();
+ void onMessage(const QString& s);
+ void onDisconnected();
+ void onError(QAbstractSocket::SocketError e);
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+};
+
+
+class NexusKeyValidator
+{
+public:
+ enum States
+ {
+ Connecting,
+ Finished,
+ InvalidJson,
+ BadResponse,
+ Timeout,
+ Cancelled,
+ Error
+ };
+
+ std::function<void (APIUserAccount)> finished;
+ std::function<void (States, QString)> stateChanged;
+
+ static QString stateToString(States s, const QString& e);
+
+ NexusKeyValidator(NXMAccessManager& am);
+ ~NexusKeyValidator();
+
+ void start(const QString& key);
+ void cancel();
+
+ bool isActive() const;
+
+private:
+ NXMAccessManager& m_manager;
+ QNetworkReply* m_reply;
+ QTimer m_timeout;
+ bool m_active;
+
+ void setState(States s, const QString& error={});
+
+ void close();
+ void abort();
+
+ void onFinished();
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+
+ void handleError(
+ int code, const QString& nexusMessage, const QString& httpError);
+};
+
/**
* @brief access manager extended to handle nxm links
@@ -36,10 +164,12 @@ class NXMAccessManager : public QNetworkAccessManager
{
Q_OBJECT
public:
+ static const std::chrono::seconds ValidationTimeout;
explicit NXMAccessManager(QObject *parent, const QString &moVersion);
- ~NXMAccessManager();
+
+ void setTopLevelWidget(QWidget* w);
bool validated() const;
@@ -53,12 +183,10 @@ public:
void clearCookies();
QString userAgent(const QString &subModule = QString()) const;
+ const QString& MOVersion() const;
- QString apiKey() const;
void clearApiKey();
- void startValidationCheck();
-
void refuseValidation();
signals:
@@ -76,17 +204,9 @@ signals:
* @param necessary true if a login was necessary and succeeded, false if the user is still logged in
**/
void validateSuccessful(bool necessary);
-
void validateFailed(const QString &message);
-
void credentialsReceived(const APIUserAccount& user);
-private slots:
-
- void validateFinished();
- void validateError(QNetworkReply::NetworkError errorCode);
- void validateTimeout();
-
protected:
virtual QNetworkReply *createRequest(
@@ -94,22 +214,22 @@ protected:
QIODevice *device);
private:
- QTimer m_ValidateTimeout;
- QNetworkReply *m_ValidateReply;
- QProgressDialog *m_ProgressDialog { nullptr };
+ enum States
+ {
+ NotChecked,
+ Valid,
+ Invalid
+ };
+ QWidget* m_TopLevel;
+ mutable ValidationProgressDialog* m_ProgressDialog;
QString m_MOVersion;
+ NexusKeyValidator m_validator;
+ States m_validationState;
- QString m_ApiKey;
-
- enum {
- VALIDATE_NOT_CHECKED,
- VALIDATE_CHECKING,
- VALIDATE_NOT_VALID,
- VALIDATE_ATTEMPT_FAILED,
- VALIDATE_REFUSED,
- VALIDATE_VALID
- } m_ValidateState = VALIDATE_NOT_CHECKED;
+ void startValidationCheck(const QString& key);
+ void onValidatorState(NexusKeyValidator::States s, const QString& e);
+ void onValidatorFinished(const APIUserAccount& user);
};
#endif // NXMACCESSMANAGER_H
diff --git a/src/organizer_en.ts b/src/organizer_en.ts
index 2a47fc18..1aa831a0 100644
--- a/src/organizer_en.ts
+++ b/src/organizer_en.ts
@@ -1664,7 +1664,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="mainwindow.ui" line="302"/>
<location filename="mainwindow.ui" line="822"/>
- <location filename="mainwindow.cpp" line="4933"/>
+ <location filename="mainwindow.cpp" line="4860"/>
<source>Create Backup</source>
<translation type="unfinished"></translation>
</message>
@@ -1840,8 +1840,8 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="mainwindow.ui" line="1044"/>
<location filename="mainwindow.ui" line="1187"/>
- <location filename="mainwindow.cpp" line="4806"/>
- <location filename="mainwindow.cpp" line="5659"/>
+ <location filename="mainwindow.cpp" line="4733"/>
+ <location filename="mainwindow.cpp" line="5551"/>
<source>Refresh</source>
<translation type="unfinished"></translation>
</message>
@@ -2120,7 +2120,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="mainwindow.ui" line="1615"/>
<location filename="mainwindow.ui" line="1618"/>
- <location filename="mainwindow.cpp" line="5687"/>
+ <location filename="mainwindow.cpp" line="5579"/>
<source>Endorse Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
@@ -2206,873 +2206,874 @@ p, li { white-space: pre-wrap; }
</message>
<message>
<location filename="mainwindow.ui" line="1736"/>
- <source>St&amp;atus bar</source>
+ <source>Status &amp;bar</source>
+ <oldsource>St&amp;atus bar</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="338"/>
+ <location filename="mainwindow.cpp" line="341"/>
<source>Toolbar and Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="339"/>
+ <location filename="mainwindow.cpp" line="342"/>
<source>Desktop</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="340"/>
+ <location filename="mainwindow.cpp" line="343"/>
<source>Start Menu</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="365"/>
+ <location filename="mainwindow.cpp" line="368"/>
<source>There is no supported sort mechanism for this game. You will probably have to use a third-party tool.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="575"/>
+ <location filename="mainwindow.cpp" line="578"/>
<source>Crash on exit</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="576"/>
+ <location filename="mainwindow.cpp" line="579"/>
<source>MO crashed while exiting. Some settings may not be saved.
Error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="926"/>
+ <location filename="mainwindow.cpp" line="929"/>
<source>There are notifications to read</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="945"/>
+ <location filename="mainwindow.cpp" line="948"/>
<source>There are no notifications</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1032"/>
- <location filename="mainwindow.cpp" line="4943"/>
- <location filename="mainwindow.cpp" line="4947"/>
+ <location filename="mainwindow.cpp" line="1035"/>
+ <location filename="mainwindow.cpp" line="4870"/>
+ <location filename="mainwindow.cpp" line="4874"/>
<source>Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1036"/>
+ <location filename="mainwindow.cpp" line="1039"/>
<source>Won&apos;t Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1053"/>
+ <location filename="mainwindow.cpp" line="1056"/>
<source>Help on UI</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1057"/>
+ <location filename="mainwindow.cpp" line="1060"/>
<source>Documentation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1061"/>
+ <location filename="mainwindow.cpp" line="1064"/>
<source>Chat on Discord</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1065"/>
+ <location filename="mainwindow.cpp" line="1068"/>
<source>Report Issue</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1069"/>
+ <location filename="mainwindow.cpp" line="1072"/>
<source>Tutorials</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1108"/>
+ <location filename="mainwindow.cpp" line="1111"/>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1109"/>
+ <location filename="mainwindow.cpp" line="1112"/>
<source>About Qt</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1168"/>
+ <location filename="mainwindow.cpp" line="1171"/>
<source>Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1169"/>
+ <location filename="mainwindow.cpp" line="1172"/>
<source>Please enter a name for the new profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1177"/>
+ <location filename="mainwindow.cpp" line="1180"/>
<source>failed to create profile: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1231"/>
+ <location filename="mainwindow.cpp" line="1234"/>
<source>Show tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1232"/>
+ <location filename="mainwindow.cpp" line="1235"/>
<source>You are starting Mod Organizer for the first time. Do you want to show a tutorial of its basic features? If you choose no you can always start the tutorial from the &quot;Help&quot;-menu.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1275"/>
+ <location filename="mainwindow.cpp" line="1278"/>
<source>Downloads in progress</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1276"/>
+ <location filename="mainwindow.cpp" line="1279"/>
<source>There are still downloads in progress, do you really want to quit?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1400"/>
+ <location filename="mainwindow.cpp" line="1403"/>
<source>Plugin &quot;%1&quot; failed: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1402"/>
+ <location filename="mainwindow.cpp" line="1405"/>
<source>Plugin &quot;%1&quot; failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1482"/>
+ <location filename="mainwindow.cpp" line="1485"/>
<source>Browse Mod Page</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1710"/>
+ <location filename="mainwindow.cpp" line="1713"/>
<source>Also in: &lt;br&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1721"/>
+ <location filename="mainwindow.cpp" line="1724"/>
<source>No conflict</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1806"/>
+ <location filename="mainwindow.cpp" line="1809"/>
<source>&lt;Edit...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2084"/>
+ <location filename="mainwindow.cpp" line="2087"/>
<source>This bsa is enabled in the ini file so it may be required!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2146"/>
+ <location filename="mainwindow.cpp" line="2149"/>
<source>Activating Network Proxy</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2249"/>
+ <location filename="mainwindow.cpp" line="2252"/>
<source>Notice: Your current MO version (%1) is lower than the previously used one (%2). The GUI may not downgrade gracefully, so you may experience oddities. However, there should be no serious issues.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2362"/>
+ <location filename="mainwindow.cpp" line="2365"/>
<source>Choose Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2363"/>
+ <location filename="mainwindow.cpp" line="2366"/>
<source>Mod Archive</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2552"/>
+ <location filename="mainwindow.cpp" line="2473"/>
<source>Start Tutorial?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2553"/>
+ <location filename="mainwindow.cpp" line="2474"/>
<source>You&apos;re about to start a tutorial. For technical reasons it&apos;s not possible to end the tutorial early. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2721"/>
+ <location filename="mainwindow.cpp" line="2648"/>
<source>failed to spawn notepad.exe: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2761"/>
+ <location filename="mainwindow.cpp" line="2688"/>
<source>failed to change origin name: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2785"/>
+ <location filename="mainwindow.cpp" line="2712"/>
<source>failed to move &quot;%1&quot; from mod &quot;%2&quot; to &quot;%3&quot;: %4</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2809"/>
+ <location filename="mainwindow.cpp" line="2736"/>
<source>&lt;Contains %1&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2844"/>
+ <location filename="mainwindow.cpp" line="2771"/>
<source>&lt;Checked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2845"/>
+ <location filename="mainwindow.cpp" line="2772"/>
<source>&lt;Unchecked&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2846"/>
+ <location filename="mainwindow.cpp" line="2773"/>
<source>&lt;Update&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2847"/>
+ <location filename="mainwindow.cpp" line="2774"/>
<source>&lt;Mod Backup&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2848"/>
+ <location filename="mainwindow.cpp" line="2775"/>
<source>&lt;Managed by MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2849"/>
+ <location filename="mainwindow.cpp" line="2776"/>
<source>&lt;Managed outside MO&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2850"/>
+ <location filename="mainwindow.cpp" line="2777"/>
<source>&lt;No category&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2851"/>
+ <location filename="mainwindow.cpp" line="2778"/>
<source>&lt;Conflicted&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2852"/>
+ <location filename="mainwindow.cpp" line="2779"/>
<source>&lt;Not Endorsed&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2898"/>
+ <location filename="mainwindow.cpp" line="2825"/>
<source>failed to rename mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2911"/>
+ <location filename="mainwindow.cpp" line="2838"/>
<source>Overwrite?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2912"/>
+ <location filename="mainwindow.cpp" line="2839"/>
<source>This will replace the existing mod &quot;%1&quot;. Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2915"/>
+ <location filename="mainwindow.cpp" line="2842"/>
<source>failed to remove mod &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="2919"/>
- <location filename="mainwindow.cpp" line="5487"/>
- <location filename="mainwindow.cpp" line="5511"/>
+ <location filename="mainwindow.cpp" line="2846"/>
+ <location filename="mainwindow.cpp" line="5379"/>
+ <location filename="mainwindow.cpp" line="5403"/>
<source>failed to rename &quot;%1&quot; to &quot;%2&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3007"/>
- <location filename="mainwindow.cpp" line="4510"/>
- <location filename="mainwindow.cpp" line="4518"/>
- <location filename="mainwindow.cpp" line="5077"/>
+ <location filename="mainwindow.cpp" line="2934"/>
+ <location filename="mainwindow.cpp" line="4437"/>
+ <location filename="mainwindow.cpp" line="4445"/>
+ <location filename="mainwindow.cpp" line="5004"/>
<source>Confirm</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3008"/>
+ <location filename="mainwindow.cpp" line="2935"/>
<source>Remove the following mods?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3023"/>
+ <location filename="mainwindow.cpp" line="2950"/>
<source>failed to remove mod: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3055"/>
- <location filename="mainwindow.cpp" line="3058"/>
- <location filename="mainwindow.cpp" line="3068"/>
+ <location filename="mainwindow.cpp" line="2982"/>
+ <location filename="mainwindow.cpp" line="2985"/>
+ <location filename="mainwindow.cpp" line="2995"/>
<source>Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3055"/>
+ <location filename="mainwindow.cpp" line="2982"/>
<source>Installation file no longer exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3059"/>
+ <location filename="mainwindow.cpp" line="2986"/>
<source>Mods installed with old versions of MO can&apos;t be reinstalled in this way.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3069"/>
+ <location filename="mainwindow.cpp" line="2996"/>
<source>Failed to create backup.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3097"/>
+ <location filename="mainwindow.cpp" line="3024"/>
<source>Endorsing multiple mods will take a while. Please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3133"/>
+ <location filename="mainwindow.cpp" line="3060"/>
<source>Unendorsing multiple mods will take a while. Please wait...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3222"/>
+ <location filename="mainwindow.cpp" line="3149"/>
<source>Failed to display overwrite dialog: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3422"/>
+ <location filename="mainwindow.cpp" line="3349"/>
<source>Opening Nexus Links</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3423"/>
+ <location filename="mainwindow.cpp" line="3350"/>
<source>You are trying to open %1 links to Nexus Mods. Are you sure you want to do this?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3450"/>
+ <location filename="mainwindow.cpp" line="3377"/>
<source>Nexus ID for this mod is unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3461"/>
+ <location filename="mainwindow.cpp" line="3388"/>
<source>Opening Web Pages</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3462"/>
+ <location filename="mainwindow.cpp" line="3389"/>
<source>You are trying to open %1 Web Pages. Are you sure you want to do this?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3668"/>
+ <location filename="mainwindow.cpp" line="3595"/>
<source>&lt;table cellspacing=&quot;5&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;All&lt;/th&gt;&lt;th&gt;Visible&lt;/th&gt;&lt;tr&gt;&lt;td&gt;Enabled mods:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%1 / %2&lt;/td&gt;&lt;td align=right&gt;%3 / %4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Unmanaged/DLCs:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%5&lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mod backups:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%7&lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Separators:&amp;emsp;&lt;/td&gt;&lt;td align=right&gt;%9&lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3723"/>
+ <location filename="mainwindow.cpp" line="3650"/>
<source>&lt;table cellspacing=&quot;6&quot;&gt;&lt;tr&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Active &lt;/th&gt;&lt;th&gt;Total&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;All plugins:&lt;/td&gt;&lt;td align=right&gt;%1 &lt;/td&gt;&lt;td align=right&gt;%2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs:&lt;/td&gt;&lt;td align=right&gt;%3 &lt;/td&gt;&lt;td align=right&gt;%4&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESPs:&lt;/td&gt;&lt;td align=right&gt;%7 &lt;/td&gt;&lt;td align=right&gt;%8&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESMs+ESPs:&lt;/td&gt;&lt;td align=right&gt;%9 &lt;/td&gt;&lt;td align=right&gt;%10&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;ESLs:&lt;/td&gt;&lt;td align=right&gt;%5 &lt;/td&gt;&lt;td align=right&gt;%6&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3755"/>
- <location filename="mainwindow.cpp" line="3893"/>
- <location filename="mainwindow.cpp" line="4868"/>
+ <location filename="mainwindow.cpp" line="3682"/>
+ <location filename="mainwindow.cpp" line="3820"/>
+ <location filename="mainwindow.cpp" line="4795"/>
<source>Create Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3756"/>
+ <location filename="mainwindow.cpp" line="3683"/>
<source>This will create an empty mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3765"/>
- <location filename="mainwindow.cpp" line="3903"/>
+ <location filename="mainwindow.cpp" line="3692"/>
+ <location filename="mainwindow.cpp" line="3830"/>
<source>A mod with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3793"/>
+ <location filename="mainwindow.cpp" line="3720"/>
<source>Create Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3794"/>
+ <location filename="mainwindow.cpp" line="3721"/>
<source>This will create a new separator.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3801"/>
+ <location filename="mainwindow.cpp" line="3728"/>
<source>A separator with this name already exists</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3894"/>
+ <location filename="mainwindow.cpp" line="3821"/>
<source>This will move all files from overwrite into a new, regular mod.
Please enter a name:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3975"/>
+ <location filename="mainwindow.cpp" line="3902"/>
<source>Move successful.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3996"/>
- <location filename="mainwindow.cpp" line="6249"/>
+ <location filename="mainwindow.cpp" line="3923"/>
+ <location filename="mainwindow.cpp" line="6141"/>
<source>Are you sure?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="3997"/>
+ <location filename="mainwindow.cpp" line="3924"/>
<source>About to recursively delete:
</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4392"/>
+ <location filename="mainwindow.cpp" line="4319"/>
<source>Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4393"/>
+ <location filename="mainwindow.cpp" line="4320"/>
<source>The versioning scheme decides which version is considered newer than another.
This function will guess the versioning scheme under the assumption that the installed version is outdated.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4413"/>
+ <location filename="mainwindow.cpp" line="4340"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4414"/>
+ <location filename="mainwindow.cpp" line="4341"/>
<source>I don&apos;t know a versioning scheme where %1 is newer than %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4510"/>
+ <location filename="mainwindow.cpp" line="4437"/>
<source>Really enable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4518"/>
+ <location filename="mainwindow.cpp" line="4445"/>
<source>Really disable all visible mods?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4595"/>
+ <location filename="mainwindow.cpp" line="4522"/>
<source>Export to csv</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4598"/>
+ <location filename="mainwindow.cpp" line="4525"/>
<source>CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet.
You can also use online editors and converters instead.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4601"/>
+ <location filename="mainwindow.cpp" line="4528"/>
<source>Select what mods you want export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4602"/>
+ <location filename="mainwindow.cpp" line="4529"/>
<source>All installed mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4603"/>
+ <location filename="mainwindow.cpp" line="4530"/>
<source>Only active (checked) mods from your current profile</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4604"/>
+ <location filename="mainwindow.cpp" line="4531"/>
<source>All currently visible mods in the mod list</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4625"/>
+ <location filename="mainwindow.cpp" line="4552"/>
<source>Choose what Columns to export:</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4628"/>
+ <location filename="mainwindow.cpp" line="4555"/>
<source>Mod_Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4630"/>
+ <location filename="mainwindow.cpp" line="4557"/>
<source>Mod_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4632"/>
+ <location filename="mainwindow.cpp" line="4559"/>
<source>Notes_column</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4633"/>
+ <location filename="mainwindow.cpp" line="4560"/>
<source>Mod_Status</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4635"/>
+ <location filename="mainwindow.cpp" line="4562"/>
<source>Primary_Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4636"/>
+ <location filename="mainwindow.cpp" line="4563"/>
<source>Nexus_ID</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4637"/>
+ <location filename="mainwindow.cpp" line="4564"/>
<source>Mod_Nexus_URL</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4638"/>
+ <location filename="mainwindow.cpp" line="4565"/>
<source>Mod_Version</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4639"/>
+ <location filename="mainwindow.cpp" line="4566"/>
<source>Install_Date</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4640"/>
+ <location filename="mainwindow.cpp" line="4567"/>
<source>Download_File_Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4748"/>
+ <location filename="mainwindow.cpp" line="4675"/>
<source>export failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4767"/>
+ <location filename="mainwindow.cpp" line="4694"/>
<source>Open Game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4769"/>
+ <location filename="mainwindow.cpp" line="4696"/>
<source>Open MyGames folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4771"/>
+ <location filename="mainwindow.cpp" line="4698"/>
<source>Open INIs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4775"/>
+ <location filename="mainwindow.cpp" line="4702"/>
<source>Open Instance folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4777"/>
+ <location filename="mainwindow.cpp" line="4704"/>
<source>Open Mods folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4779"/>
+ <location filename="mainwindow.cpp" line="4706"/>
<source>Open Profile folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4781"/>
+ <location filename="mainwindow.cpp" line="4708"/>
<source>Open Downloads folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4785"/>
+ <location filename="mainwindow.cpp" line="4712"/>
<source>Open MO2 Install folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4787"/>
+ <location filename="mainwindow.cpp" line="4714"/>
<source>Open MO2 Plugins folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4789"/>
+ <location filename="mainwindow.cpp" line="4716"/>
<source>Open MO2 Logs folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4797"/>
+ <location filename="mainwindow.cpp" line="4724"/>
<source>Install Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4798"/>
+ <location filename="mainwindow.cpp" line="4725"/>
<source>Create empty mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4799"/>
+ <location filename="mainwindow.cpp" line="4726"/>
<source>Create Separator</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4803"/>
+ <location filename="mainwindow.cpp" line="4730"/>
<source>Enable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4804"/>
+ <location filename="mainwindow.cpp" line="4731"/>
<source>Disable all visible</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4805"/>
+ <location filename="mainwindow.cpp" line="4732"/>
<source>Check for updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4807"/>
+ <location filename="mainwindow.cpp" line="4734"/>
<source>Export to csv...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4816"/>
- <location filename="mainwindow.cpp" line="4832"/>
+ <location filename="mainwindow.cpp" line="4743"/>
+ <location filename="mainwindow.cpp" line="4759"/>
<source>Send to</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4817"/>
- <location filename="mainwindow.cpp" line="4833"/>
+ <location filename="mainwindow.cpp" line="4744"/>
+ <location filename="mainwindow.cpp" line="4760"/>
<source>Top</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4818"/>
- <location filename="mainwindow.cpp" line="4834"/>
+ <location filename="mainwindow.cpp" line="4745"/>
+ <location filename="mainwindow.cpp" line="4761"/>
<source>Bottom</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4819"/>
- <location filename="mainwindow.cpp" line="4835"/>
+ <location filename="mainwindow.cpp" line="4746"/>
+ <location filename="mainwindow.cpp" line="4762"/>
<source>Priority...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4820"/>
+ <location filename="mainwindow.cpp" line="4747"/>
<source>Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4859"/>
+ <location filename="mainwindow.cpp" line="4786"/>
<source>All Mods</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4867"/>
+ <location filename="mainwindow.cpp" line="4794"/>
<source>Sync to Mods...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4869"/>
+ <location filename="mainwindow.cpp" line="4796"/>
<source>Move content to Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4870"/>
+ <location filename="mainwindow.cpp" line="4797"/>
<source>Clear Overwrite...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4872"/>
- <location filename="mainwindow.cpp" line="4995"/>
+ <location filename="mainwindow.cpp" line="4799"/>
+ <location filename="mainwindow.cpp" line="4922"/>
<source>Open in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4874"/>
+ <location filename="mainwindow.cpp" line="4801"/>
<source>Restore Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4875"/>
+ <location filename="mainwindow.cpp" line="4802"/>
<source>Remove Backup...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4878"/>
- <location filename="mainwindow.cpp" line="4897"/>
+ <location filename="mainwindow.cpp" line="4805"/>
+ <location filename="mainwindow.cpp" line="4824"/>
<source>Change Categories</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4882"/>
- <location filename="mainwindow.cpp" line="4902"/>
+ <location filename="mainwindow.cpp" line="4809"/>
+ <location filename="mainwindow.cpp" line="4829"/>
<source>Primary Category</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4886"/>
+ <location filename="mainwindow.cpp" line="4813"/>
<source>Rename Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4887"/>
+ <location filename="mainwindow.cpp" line="4814"/>
<source>Remove Separator...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4890"/>
+ <location filename="mainwindow.cpp" line="4817"/>
<source>Select Color...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4892"/>
+ <location filename="mainwindow.cpp" line="4819"/>
<source>Reset Color</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4909"/>
+ <location filename="mainwindow.cpp" line="4836"/>
<source>Change versioning scheme</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4913"/>
+ <location filename="mainwindow.cpp" line="4840"/>
<source>Force-check updates</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4915"/>
+ <location filename="mainwindow.cpp" line="4842"/>
<source>Un-ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4918"/>
+ <location filename="mainwindow.cpp" line="4845"/>
<source>Ignore update</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4923"/>
- <location filename="mainwindow.cpp" line="6373"/>
+ <location filename="mainwindow.cpp" line="4850"/>
+ <location filename="mainwindow.cpp" line="6265"/>
<source>Enable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4924"/>
- <location filename="mainwindow.cpp" line="6374"/>
+ <location filename="mainwindow.cpp" line="4851"/>
+ <location filename="mainwindow.cpp" line="6266"/>
<source>Disable selected</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4930"/>
+ <location filename="mainwindow.cpp" line="4857"/>
<source>Rename Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4931"/>
+ <location filename="mainwindow.cpp" line="4858"/>
<source>Reinstall Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4932"/>
+ <location filename="mainwindow.cpp" line="4859"/>
<source>Remove Mod...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4940"/>
+ <location filename="mainwindow.cpp" line="4867"/>
<source>Un-Endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4944"/>
+ <location filename="mainwindow.cpp" line="4871"/>
<source>Won&apos;t endorse</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4950"/>
+ <location filename="mainwindow.cpp" line="4877"/>
<source>Endorsement state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4960"/>
+ <location filename="mainwindow.cpp" line="4887"/>
<source>Start tracking</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4963"/>
+ <location filename="mainwindow.cpp" line="4890"/>
<source>Stop tracking</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4966"/>
+ <location filename="mainwindow.cpp" line="4893"/>
<source>Tracked state unknown</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4977"/>
+ <location filename="mainwindow.cpp" line="4904"/>
<source>Ignore missing data</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4981"/>
+ <location filename="mainwindow.cpp" line="4908"/>
<source>Mark as converted/working</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4985"/>
+ <location filename="mainwindow.cpp" line="4912"/>
<source>Visit on Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4991"/>
+ <location filename="mainwindow.cpp" line="4918"/>
<source>Visit on %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="4999"/>
+ <location filename="mainwindow.cpp" line="4926"/>
<source>Information...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5006"/>
- <location filename="mainwindow.cpp" line="6426"/>
+ <location filename="mainwindow.cpp" line="4933"/>
+ <location filename="mainwindow.cpp" line="6318"/>
<source>Exception: </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5008"/>
- <location filename="mainwindow.cpp" line="6428"/>
+ <location filename="mainwindow.cpp" line="4935"/>
+ <location filename="mainwindow.cpp" line="6320"/>
<source>Unknown exception</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5037"/>
+ <location filename="mainwindow.cpp" line="4964"/>
<source>&lt;All&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5039"/>
+ <location filename="mainwindow.cpp" line="4966"/>
<source>&lt;Multiple&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5074"/>
+ <location filename="mainwindow.cpp" line="5001"/>
<source>%1 more</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="5078"/>
+ <location filename="mainwindow.cpp" line="5005"/>
<source>Are you sure you want to remove the following %n save(s)?&lt;br&gt;&lt;ul&gt;%1&lt;/ul&gt;&lt;br&gt;Removed saves will be sent to the Recycle Bin.</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -3080,12 +3081,12 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5123"/>
+ <location filename="mainwindow.cpp" line="5050"/>
<source>Enable Mods...</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
- <location filename="mainwindow.cpp" line="5138"/>
+ <location filename="mainwindow.cpp" line="5065"/>
<source>Delete %n save(s)</source>
<translation type="unfinished">
<numerusform></numerusform>
@@ -3093,22 +3094,12 @@ You can also use online editors and converters instead.</source>
</translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5180"/>
- <source>failed to remove %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5202"/>
- <source>failed to create %1</source>
- <translation type="unfinished"></translation>
- </message>
- <message>
- <location filename="mainwindow.cpp" line="5232"/>
+ <location filename="mainwindow.cpp" line="5124"/>
<source>Restarting MO</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5233"/>
+ <location filename="mainwindow.cpp" line="5125"/>
<source>Changing the managed game directory requires restarting MO.
Any pending downloads will be paused.
@@ -3116,348 +3107,348 @@ Click OK to restart MO now.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5253"/>
+ <location filename="mainwindow.cpp" line="5145"/>
<source>Can&apos;t change download directory while downloads are in progress!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5400"/>
+ <location filename="mainwindow.cpp" line="5292"/>
<source>failed to write to file %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5406"/>
+ <location filename="mainwindow.cpp" line="5298"/>
<source>%1 written</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5430"/>
+ <location filename="mainwindow.cpp" line="5322"/>
<source>Enter Name</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5431"/>
+ <location filename="mainwindow.cpp" line="5323"/>
<source>Please enter a name for the executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5450"/>
+ <location filename="mainwindow.cpp" line="5342"/>
<source>Not an executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5450"/>
+ <location filename="mainwindow.cpp" line="5342"/>
<source>This is not a recognized executable.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5472"/>
- <location filename="mainwindow.cpp" line="5497"/>
+ <location filename="mainwindow.cpp" line="5364"/>
+ <location filename="mainwindow.cpp" line="5389"/>
<source>Replace file?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5472"/>
+ <location filename="mainwindow.cpp" line="5364"/>
<source>There already is a hidden version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5475"/>
- <location filename="mainwindow.cpp" line="5500"/>
+ <location filename="mainwindow.cpp" line="5367"/>
+ <location filename="mainwindow.cpp" line="5392"/>
<source>File operation failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5475"/>
- <location filename="mainwindow.cpp" line="5500"/>
+ <location filename="mainwindow.cpp" line="5367"/>
+ <location filename="mainwindow.cpp" line="5392"/>
<source>Failed to remove &quot;%1&quot;. Maybe you lack the required file permissions?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5497"/>
+ <location filename="mainwindow.cpp" line="5389"/>
<source>There already is a visible version of this file. Replace it?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5541"/>
- <location filename="mainwindow.cpp" line="7051"/>
+ <location filename="mainwindow.cpp" line="5433"/>
+ <location filename="mainwindow.cpp" line="6928"/>
<source>Set Priority</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5541"/>
+ <location filename="mainwindow.cpp" line="5433"/>
<source>Set the priority of the selected plugins</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5605"/>
+ <location filename="mainwindow.cpp" line="5497"/>
<source>Update available</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5632"/>
+ <location filename="mainwindow.cpp" line="5524"/>
<source>Open/Execute</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5633"/>
+ <location filename="mainwindow.cpp" line="5525"/>
<source>Add as Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5637"/>
+ <location filename="mainwindow.cpp" line="5529"/>
<source>Preview</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5650"/>
+ <location filename="mainwindow.cpp" line="5542"/>
<source>Un-Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5652"/>
+ <location filename="mainwindow.cpp" line="5544"/>
<source>Hide</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5658"/>
+ <location filename="mainwindow.cpp" line="5550"/>
<source>Write To File...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5688"/>
+ <location filename="mainwindow.cpp" line="5580"/>
<source>Do you want to endorse Mod Organizer on %1 now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5702"/>
+ <location filename="mainwindow.cpp" line="5594"/>
<source>Abstain from Endorsing Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5703"/>
+ <location filename="mainwindow.cpp" line="5595"/>
<source>Are you sure you want to abstain from endorsing Mod Organizer 2?
You will have to visit the mod page on the %1 Nexus site to change your mind.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5783"/>
- <location filename="mainwindow.cpp" line="5784"/>
+ <location filename="mainwindow.cpp" line="5675"/>
+ <location filename="mainwindow.cpp" line="5676"/>
<source>Thank you for endorsing MO2! :)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="5786"/>
- <location filename="mainwindow.cpp" line="5787"/>
+ <location filename="mainwindow.cpp" line="5678"/>
+ <location filename="mainwindow.cpp" line="5679"/>
<source>Please reconsider endorsing MO2 on Nexus!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6022"/>
+ <location filename="mainwindow.cpp" line="5914"/>
<source>Thank you!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6022"/>
+ <location filename="mainwindow.cpp" line="5914"/>
<source>Thank you for your endorsement!</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6025"/>
+ <location filename="mainwindow.cpp" line="5917"/>
<source>Okay.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6025"/>
+ <location filename="mainwindow.cpp" line="5917"/>
<source>This mod will not be endorsed and will no longer ask you to endorse.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6084"/>
+ <location filename="mainwindow.cpp" line="5976"/>
<source>Mod ID %1 no longer seems to be available on Nexus.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6086"/>
+ <location filename="mainwindow.cpp" line="5978"/>
<source>Request to Nexus failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6102"/>
- <location filename="mainwindow.cpp" line="6164"/>
+ <location filename="mainwindow.cpp" line="5994"/>
+ <location filename="mainwindow.cpp" line="6056"/>
<source>failed to read %1: %2</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6114"/>
- <location filename="mainwindow.cpp" line="6616"/>
+ <location filename="mainwindow.cpp" line="6006"/>
+ <location filename="mainwindow.cpp" line="6493"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6114"/>
+ <location filename="mainwindow.cpp" line="6006"/>
<source>failed to extract %1 (errorcode %2)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6146"/>
+ <location filename="mainwindow.cpp" line="6038"/>
<source>Extract BSA</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6175"/>
+ <location filename="mainwindow.cpp" line="6067"/>
<source>This archive contains invalid hashes. Some files may be broken.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6221"/>
+ <location filename="mainwindow.cpp" line="6113"/>
<source>Extract...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6250"/>
+ <location filename="mainwindow.cpp" line="6142"/>
<source>This will restart MO, continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6297"/>
+ <location filename="mainwindow.cpp" line="6189"/>
<source>Edit Categories...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6298"/>
+ <location filename="mainwindow.cpp" line="6190"/>
<source>Deselect filter</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6357"/>
+ <location filename="mainwindow.cpp" line="6249"/>
<source>Remove &apos;%1&apos; from the toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6378"/>
+ <location filename="mainwindow.cpp" line="6270"/>
<source>Enable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6379"/>
+ <location filename="mainwindow.cpp" line="6271"/>
<source>Disable all</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6400"/>
+ <location filename="mainwindow.cpp" line="6292"/>
<source>Unlock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6403"/>
+ <location filename="mainwindow.cpp" line="6295"/>
<source>Lock load order</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6413"/>
+ <location filename="mainwindow.cpp" line="6305"/>
<source>Open Origin in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6418"/>
+ <location filename="mainwindow.cpp" line="6310"/>
<source>Open Origin Info...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6562"/>
+ <location filename="mainwindow.cpp" line="6439"/>
<source>depends on missing &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6566"/>
+ <location filename="mainwindow.cpp" line="6443"/>
<source>incompatible with &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6592"/>
+ <location filename="mainwindow.cpp" line="6469"/>
<source>Please wait while LOOT is running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6689"/>
+ <location filename="mainwindow.cpp" line="6566"/>
<source>loot failed. Exit code was: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6711"/>
+ <location filename="mainwindow.cpp" line="6588"/>
<source>failed to start loot</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6714"/>
+ <location filename="mainwindow.cpp" line="6591"/>
<source>failed to run loot: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6718"/>
+ <location filename="mainwindow.cpp" line="6595"/>
<source>Errors occurred</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6765"/>
+ <location filename="mainwindow.cpp" line="6642"/>
<source>Backup of load order created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6775"/>
+ <location filename="mainwindow.cpp" line="6652"/>
<source>Choose backup to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6788"/>
+ <location filename="mainwindow.cpp" line="6665"/>
<source>No Backups</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6788"/>
+ <location filename="mainwindow.cpp" line="6665"/>
<source>There are no backups to restore</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6809"/>
- <location filename="mainwindow.cpp" line="6831"/>
+ <location filename="mainwindow.cpp" line="6686"/>
+ <location filename="mainwindow.cpp" line="6708"/>
<source>Restore failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6810"/>
- <location filename="mainwindow.cpp" line="6832"/>
+ <location filename="mainwindow.cpp" line="6687"/>
+ <location filename="mainwindow.cpp" line="6709"/>
<source>Failed to restore the backup. Errorcode: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6821"/>
+ <location filename="mainwindow.cpp" line="6698"/>
<source>Backup of mod list created</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6927"/>
+ <location filename="mainwindow.cpp" line="6804"/>
<source>A file with the same name has already been downloaded. What would you like to do?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6929"/>
+ <location filename="mainwindow.cpp" line="6806"/>
<source>Overwrite</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6930"/>
+ <location filename="mainwindow.cpp" line="6807"/>
<source>Rename new file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="6931"/>
+ <location filename="mainwindow.cpp" line="6808"/>
<source>Ignore file</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="7051"/>
+ <location filename="mainwindow.cpp" line="6928"/>
<source>Set the priority of the selected mods</source>
<translation type="unfinished"></translation>
</message>
@@ -3560,7 +3551,8 @@ You will have to visit the mod page on the %1 Nexus site to change your mind.</s
</message>
<message>
<location filename="modinfo.cpp" line="325"/>
- <source>You have mods that haven&apos;t been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</source>
+ <source>You have mods that haven&apos;t been checked within the last month using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</source>
+ <oldsource>You have mods that haven&apos;t been checked within 30 days using the new API. These mods must be checked before we can use the bulk update API. This will consume significantly more API requests than usual. You will need to rerun the update check once complete in order to parse the remaining mods.</oldsource>
<translation type="unfinished"></translation>
</message>
</context>
@@ -4522,227 +4514,227 @@ p, li { white-space: pre-wrap; }
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="943"/>
- <location filename="organizercore.cpp" line="980"/>
- <location filename="organizercore.cpp" line="991"/>
- <location filename="organizercore.cpp" line="1049"/>
+ <location filename="organizercore.cpp" line="958"/>
+ <location filename="organizercore.cpp" line="995"/>
+ <location filename="organizercore.cpp" line="1006"/>
+ <location filename="organizercore.cpp" line="1064"/>
<source>Installation cancelled</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="944"/>
- <location filename="organizercore.cpp" line="992"/>
+ <location filename="organizercore.cpp" line="959"/>
+ <location filename="organizercore.cpp" line="1007"/>
<source>Another installation is currently in progress.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="956"/>
- <location filename="organizercore.cpp" line="1021"/>
+ <location filename="organizercore.cpp" line="971"/>
+ <location filename="organizercore.cpp" line="1036"/>
<source>Installation successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="964"/>
- <location filename="organizercore.cpp" line="1031"/>
+ <location filename="organizercore.cpp" line="979"/>
+ <location filename="organizercore.cpp" line="1046"/>
<source>Configure Mod</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="965"/>
- <location filename="organizercore.cpp" line="1032"/>
+ <location filename="organizercore.cpp" line="980"/>
+ <location filename="organizercore.cpp" line="1047"/>
<source>This mod contains ini tweaks. Do you want to configure them now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="977"/>
- <location filename="organizercore.cpp" line="1042"/>
+ <location filename="organizercore.cpp" line="992"/>
+ <location filename="organizercore.cpp" line="1057"/>
<source>mod not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="981"/>
- <location filename="organizercore.cpp" line="1050"/>
+ <location filename="organizercore.cpp" line="996"/>
+ <location filename="organizercore.cpp" line="1065"/>
<source>The mod was not installed completely.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1320"/>
+ <location filename="organizercore.cpp" line="1335"/>
<source>file not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1333"/>
+ <location filename="organizercore.cpp" line="1348"/>
<source>failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1394"/>
+ <location filename="organizercore.cpp" line="1409"/>
<source>Sorry</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1395"/>
+ <location filename="organizercore.cpp" line="1410"/>
<source>Sorry, can&apos;t preview anything. This function currently does not support extracting from bsas.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1405"/>
+ <location filename="organizercore.cpp" line="1420"/>
<source>File &apos;%1&apos; not found.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1413"/>
+ <location filename="organizercore.cpp" line="1428"/>
<source>Failed to generate preview for %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1510"/>
+ <location filename="organizercore.cpp" line="1525"/>
<source>Executable not found: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1545"/>
+ <location filename="organizercore.cpp" line="1560"/>
<source>Start Steam?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1546"/>
+ <location filename="organizercore.cpp" line="1561"/>
<source>Steam is required to be running already to correctly start the game. Should MO try to start steam now?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1569"/>
+ <location filename="organizercore.cpp" line="1584"/>
<source>Steam: Access Denied</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1570"/>
+ <location filename="organizercore.cpp" line="1585"/>
<source>MO was denied access to the Steam process. This normally indicates that Steam is being run as administrator while MO is not. This can cause issues launching the game. It is recommended to not run Steam as administrator unless absolutely necessary.
Restart MO as administrator?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1617"/>
+ <location filename="organizercore.cpp" line="1632"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1625"/>
+ <location filename="organizercore.cpp" line="1640"/>
<source>Windows Event Log Error</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1626"/>
+ <location filename="organizercore.cpp" line="1641"/>
<source>The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed.
Continue launching %1?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1638"/>
+ <location filename="organizercore.cpp" line="1653"/>
<source>Blacklisted Executable</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1639"/>
+ <location filename="organizercore.cpp" line="1654"/>
<source>The executable you are attempted to launch is blacklisted in the virtual file system. This will likely prevent the executable, and any executables that are launched by this one, from seeing any mods. This could extend to INI files, save games and any other virtualized files.
Continue launching %1?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1737"/>
+ <location filename="organizercore.cpp" line="1752"/>
<source>No profile set</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2044"/>
+ <location filename="organizercore.cpp" line="2059"/>
<source>Failed to refresh list of esps: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2153"/>
+ <location filename="organizercore.cpp" line="2168"/>
<source>Multiple esps/esls activated, please check that they don&apos;t conflict.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2217"/>
+ <location filename="organizercore.cpp" line="2232"/>
<source>You need to be logged in with Nexus</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2256"/>
+ <location filename="organizercore.cpp" line="2271"/>
<source>Download?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2257"/>
+ <location filename="organizercore.cpp" line="2272"/>
<source>A download has been started but no installed page plugin recognizes it.
If you download anyway no information (i.e. version) will be associated with the download.
Continue?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2392"/>
- <location filename="organizercore.cpp" line="2441"/>
+ <location filename="organizercore.cpp" line="2407"/>
+ <location filename="organizercore.cpp" line="2456"/>
<source>failed to update mod list: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2448"/>
- <location filename="organizercore.cpp" line="2465"/>
+ <location filename="organizercore.cpp" line="2463"/>
+ <location filename="organizercore.cpp" line="2480"/>
<source>login successful</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2472"/>
+ <location filename="organizercore.cpp" line="2487"/>
<source>Login failed</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2473"/>
+ <location filename="organizercore.cpp" line="2488"/>
<source>Login failed, try again?</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2482"/>
+ <location filename="organizercore.cpp" line="2497"/>
<source>login failed: %1. Download will not be associated with an account</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2490"/>
+ <location filename="organizercore.cpp" line="2505"/>
<source>login failed: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2500"/>
+ <location filename="organizercore.cpp" line="2515"/>
<source>login failed: %1. You need to log-in with Nexus to update MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2553"/>
+ <location filename="organizercore.cpp" line="2568"/>
<source>MO1 &quot;Script Extender&quot; load mechanism has left hook.dll in your game folder</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2556"/>
- <location filename="organizercore.cpp" line="2572"/>
+ <location filename="organizercore.cpp" line="2571"/>
+ <location filename="organizercore.cpp" line="2587"/>
<source>Description missing</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2565"/>
+ <location filename="organizercore.cpp" line="2580"/>
<source>&lt;a href=&quot;%1&quot;&gt;hook.dll&lt;/a&gt; has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to &quot;Script Extender&quot;, in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2599"/>
+ <location filename="organizercore.cpp" line="2614"/>
<source>failed to save load order: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="2671"/>
+ <location filename="organizercore.cpp" line="2686"/>
<source>The designated write target &quot;%1&quot; is not enabled.</source>
<translation type="unfinished"></translation>
</message>
@@ -5425,6 +5417,7 @@ p, li { white-space: pre-wrap; }
<location filename="../../uibase/src/report.cpp" line="37"/>
<location filename="main.cpp" line="98"/>
<location filename="organizercore.cpp" line="684"/>
+ <location filename="organizercore.cpp" line="699"/>
<source>Error</source>
<translation type="unfinished"></translation>
</message>
@@ -5787,88 +5780,95 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="315"/>
+ <location filename="main.cpp" line="308"/>
+ <location filename="main.cpp" line="346"/>
+ <location filename="main.cpp" line="360"/>
+ <source>The configured path to the game directory (%1) appears to be a symbolic (or other) link. This setup is incompatible with MO2&apos;s VFS and will not run correctly.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="main.cpp" line="320"/>
<source>Could not use configuration settings for game &quot;%1&quot;, path &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="319"/>
- <location filename="main.cpp" line="342"/>
- <location filename="main.cpp" line="359"/>
+ <location filename="main.cpp" line="324"/>
+ <location filename="main.cpp" line="353"/>
+ <location filename="main.cpp" line="375"/>
<source>Please select the installation of %1 to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="320"/>
- <location filename="main.cpp" line="343"/>
- <location filename="main.cpp" line="360"/>
+ <location filename="main.cpp" line="325"/>
+ <location filename="main.cpp" line="354"/>
+ <location filename="main.cpp" line="376"/>
<source>Please select the game to manage</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="368"/>
+ <location filename="main.cpp" line="384"/>
<source>Canceled finding %1 in &quot;%2&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="369"/>
+ <location filename="main.cpp" line="385"/>
<source>Canceled finding game in &quot;%1&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="375"/>
+ <location filename="main.cpp" line="391"/>
<source>%1 not identified in &quot;%2&quot;. The directory is required to contain the game binary.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="383"/>
+ <location filename="main.cpp" line="399"/>
<source>No game identified in &quot;%1&quot;. The directory is required to contain the game binary.&lt;br&gt;&lt;br&gt;&lt;b&gt;These are the games supported by Mod Organizer:&lt;/b&gt;&lt;ul&gt;%2&lt;/ul&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="582"/>
+ <location filename="main.cpp" line="598"/>
<source>Please select the game edition you have (MO can&apos;t start the game correctly if this is set incorrectly!)</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="619"/>
+ <location filename="main.cpp" line="635"/>
<source>failed to start shortcut: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="641"/>
+ <location filename="main.cpp" line="657"/>
<source>failed to start application: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="789"/>
+ <location filename="main.cpp" line="805"/>
<location filename="settings.cpp" line="1186"/>
<source>Mod Organizer</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="790"/>
+ <location filename="main.cpp" line="806"/>
<source>An instance of Mod Organizer is already running</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="main.cpp" line="805"/>
+ <location filename="main.cpp" line="821"/>
<source>Failed to set up instance</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1244"/>
+ <location filename="mainwindow.cpp" line="1247"/>
<source>Please use &quot;Help&quot; from the toolbar to get usage instructions to all elements</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1770"/>
- <location filename="mainwindow.cpp" line="5355"/>
+ <location filename="mainwindow.cpp" line="1773"/>
+ <location filename="mainwindow.cpp" line="5247"/>
<source>&lt;Manage...&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="mainwindow.cpp" line="1782"/>
+ <location filename="mainwindow.cpp" line="1785"/>
<source>failed to parse profile %1: %2</source>
<translation type="unfinished"></translation>
</message>
@@ -5919,12 +5919,17 @@ If the folder was still in use, restart MO and try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1234"/>
+ <location filename="organizercore.cpp" line="700"/>
+ <source>One of the configured MO2 directories (profiles, mods, or overwrite) is on a path containing a symbolic (or other) link. This is incompatible with MO2&apos;s VFS system.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="organizercore.cpp" line="1249"/>
<source>Select binary</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="organizercore.cpp" line="1235"/>
+ <location filename="organizercore.cpp" line="1250"/>
<source>Binary</source>
<translation type="unfinished"></translation>
</message>
@@ -7407,88 +7412,88 @@ There IS a notification now but you may want to hold off on clearing it until af
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="39"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="40"/>
<source>Finally there are tooltips on almost every part of Mod Organizer. If there is a control in MO you don&apos;t understand, please try hovering over it to get a short description or use &quot;Help on UI&quot; from the help menu to get a longer explanation</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="46"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="47"/>
<source>This list displays all mods installed through MO. It also displays installed DLCs and some mods installed outside MO but you have limited control over those in MO.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="53"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="54"/>
<source>Before we start installing mods, let&apos;s have a quick look at the settings.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="65"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="66"/>
<source>Now it&apos;s time to install a few mods!Please go along with this because we need a few mods installed to demonstrate other features</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="71"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="72"/>
<source>There are a few ways to get mods into ModOrganizer. If you associated MO with NXM links in the settings you can now use your regular browser to send downloads from Nexus to MO. Click on &quot;Nexus&quot; to open nexus, find a mod and click the green download buttons on Nexus saying &quot;Download with Manager&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="83"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="84"/>
<source>You can also install mods from disk using the &quot;Install Mod&quot; button.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="89"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="90"/>
<source>Downloads will appear on the &quot;Downloads&quot;-tab here. You have to download and install at least one mod to proceed.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="97"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="98"/>
<source>Great, you just installed your first mod. Please note that the installation procedure may differ based on how a mod was packaged.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="103"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="104"/>
<source>Now you know all about downloading and installing mods but they are not enabled yet...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="108"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="109"/>
<source>Install a few more mods if you want, then enable mods by checking them in the left pane. Mods that aren&apos;t enabled have no effect on the game whatsoever. </source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="117"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="118"/>
<source>For some mods, enabling it on the left pane is all you have to do...</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="122"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="123"/>
<source>...but most contain plugins. These are plugins for the game and are required to add stuff to the game (new weapons, armors, quests, areas, ...). Please open the &quot;Plugins&quot;-tab to get a list of plugins.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="133"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="134"/>
<source>You will notice some plugins are grayed out. These are part of the main game and can&apos;t be disabled.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="139"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="140"/>
<source>A single mod may contain zero, one or multiple esps. Some or all may be optional. If in doubt, please consult the documentation of the individual mod. To do so, right-click the mod and select &quot;Information&quot;.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="148"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="149"/>
<source>Now you know how to download, install and enable mods.
It&apos;s important you always start the game from inside MO, otherwise the mods you installed here won&apos;t work.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="156"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="157"/>
<source>This combobox lets you choose &lt;i&gt;what&lt;/i&gt; to start. This way you can start the game, Launcher, Script Extender, the Creation Kit or other tools. If you miss a tool you can also configure this list but that is an advanced topic.</source>
<translation type="unfinished"></translation>
</message>
<message>
- <location filename="tutorials/tutorial_firststeps_main.js" line="164"/>
+ <location filename="tutorials/tutorial_firststeps_main.js" line="165"/>
<source>This completes the basic tutorial. As homework go play a bit! After you have installed more mods you may want to read the tutorial on conflict resolution.</source>
<translation type="unfinished"></translation>
</message>
@@ -7526,7 +7531,8 @@ Please open the &quot;Nexus&quot;-tab</source>
</message>
<message>
<location filename="tutorials/tutorial_firststeps_settings.js" line="19"/>
- <source>Use this interface to obtain an API key from NexusMods.This is used for all API connections - downloads, updatesetc. MO2 uses the Windows Credential Manager to storethis data securely. If the SSO page on Nexus is failing,use the manual entry and copy the API key from your profile.</source>
+ <source>Use this interface to obtain an API key from NexusMods. This is used for all API connections - downloads, updates etc. MO2 uses the Windows Credential Manager to store this data securely. If the SSO page on Nexus is failing, use the manual entry and copy the API key from your profile.</source>
+ <oldsource>Use this interface to obtain an API key from NexusMods.This is used for all API connections - downloads, updatesetc. MO2 uses the Windows Credential Manager to storethis data securely. If the SSO page on Nexus is failing,use the manual entry and copy the API key from your profile.</oldsource>
<translation type="unfinished"></translation>
</message>
</context>
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index 81ec7f43..eeb69e61 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -2484,6 +2484,9 @@ void OrganizerCore::loginSuccessfulUpdate(bool necessary)
void OrganizerCore::loginFailed(const QString &message)
{
+ qCritical().nospace().noquote()
+ << "Nexus API validation failed: " << message;
+
if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"),
tr("Login failed, try again?"))
== QMessageBox::Yes) {
diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp
index a9ef8bef..ff4099cf 100644
--- a/src/overwriteinfodialog.cpp
+++ b/src/overwriteinfodialog.cpp
@@ -289,5 +289,6 @@ void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &
m_FileSelection.clear();
m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0));
}
- menu.exec(ui->filesView->mapToGlobal(pos));
+
+ menu.exec(ui->filesView->viewport()->mapToGlobal(pos));
}
diff --git a/src/pch.h b/src/pch.h
index 9640b09d..504ef8f1 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -97,6 +97,7 @@
#include <QDirIterator>
#include <QDragEnterEvent>
#include <QDropEvent>
+#include <QElapsedTimer>
#include <QEvent>
#include <QFile>
#include <QFileDialog>
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp
index 271c621b..e967b27c 100644
--- a/src/selfupdater.cpp
+++ b/src/selfupdater.cpp
@@ -130,7 +130,7 @@ void SelfUpdater::testForUpdate()
m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
[this](const QJsonArray &releases) {
if (releases.isEmpty()) {
- qDebug("Unable to connect to github.com to check version");
+ // error message already logged
return;
}
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 95e4ceb0..0dae31ac 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -28,7 +28,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "settings.h"
#include "instancemanager.h"
#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
#include "plugincontainer.h"
#include <boost/uuid/uuid_generators.hpp>
@@ -49,7 +48,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
-
class NexusManualKeyDialog : public QDialog
{
public:
@@ -57,6 +55,7 @@ public:
: QDialog(parent), ui(new Ui::NexusManualKeyDialog)
{
ui->setupUi(this);
+ ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); });
connect(ui->paste, &QPushButton::clicked, [&]{ paste(); });
@@ -103,30 +102,21 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* setti
, ui(new Ui::SettingsDialog)
, m_settings(settings)
, m_PluginContainer(pluginContainer)
- , m_nexusLogin(new QWebSocket)
- , m_KeyReceived(false)
- , m_KeyCleared(false)
+ , m_keyChanged(false)
, m_GeometriesReset(false)
{
ui->setupUi(this);
ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
- QShortcut *delShortcut
- = new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
+ QShortcut *delShortcut = new QShortcut(
+ QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem()));
- connect(m_nexusLogin, SIGNAL(connected()), this, SLOT(dispatchLogin()));
- connect(m_nexusLogin, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(authError(QAbstractSocket::SocketError)));
- connect(m_nexusLogin, SIGNAL(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &)));
- connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection()));
- m_loginTimer.callOnTimeout(this, &SettingsDialog::loginPing);
- updateNexusButtons();
+ updateNexusState();
}
SettingsDialog::~SettingsDialog()
{
- m_loginTimer.stop();
- m_nexusLogin->close();
disconnect(this);
delete ui;
}
@@ -188,7 +178,7 @@ bool SettingsDialog::getResetGeometries()
bool SettingsDialog::getApiKeyChanged()
{
- return m_KeyReceived || m_KeyCleared;
+ return m_keyChanged;
}
void SettingsDialog::on_categoriesBtn_clicked()
@@ -391,167 +381,218 @@ void SettingsDialog::on_resetDialogsButton_clicked()
void SettingsDialog::on_nexusConnect_clicked()
{
- fetchNexusApiKey();
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
+ m_nexusLogin->cancel();
+ return;
+ }
+
+ if (!m_nexusLogin) {
+ m_nexusLogin.reset(new NexusSSOLogin);
+
+ m_nexusLogin->keyChanged = [&](auto&& s){
+ onSSOKeyChanged(s);
+ };
+
+ m_nexusLogin->stateChanged = [&](auto&& s, auto&& e){
+ onSSOStateChanged(s, e);
+ };
+ }
+
+ ui->nexusLog->clear();
+ m_nexusLogin->start();
+ updateNexusState();
}
void SettingsDialog::on_nexusManualKey_clicked()
{
- NexusManualKeyDialog dialog(this);
+ if (m_nexusValidator && m_nexusValidator->isActive()) {
+ m_nexusValidator->cancel();
+ return;
+ }
+ NexusManualKeyDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const auto key = dialog.key();
-
if (key.isEmpty()) {
clearKey();
- } else {
- if (setKey(key)) {
- testApiKey();
- }
+ return;
}
+
+ ui->nexusLog->clear();
+ validateKey(key);
}
-void SettingsDialog::fetchNexusApiKey()
+void SettingsDialog::on_nexusDisconnect_clicked()
{
- QUrl url = QUrl("wss://sso.nexusmods.com");
- m_nexusLogin->open(url);
- updateNexusButtons();
+ clearKey();
+ ui->nexusLog->clear();
+ addNexusLog(tr("Disconnected."));
}
-void SettingsDialog::dispatchLogin()
+void SettingsDialog::validateKey(const QString& key)
{
- m_KeyReceived = false;
- QJsonObject login;
- if (m_UUID.isEmpty()) {
- boost::uuids::random_generator generator;
- boost::uuids::uuid sessionId = generator();
- m_UUID = boost::uuids::to_string(sessionId).c_str();
+ if (!m_nexusValidator) {
+ m_nexusValidator.reset(new NexusKeyValidator(
+ *NexusInterface::instance(m_PluginContainer)->getAccessManager()));
+
+ m_nexusValidator->stateChanged = [&](auto&& s, auto&& e){
+ onValidatorStateChanged(s, e);
+ };
+
+ m_nexusValidator->finished = [&](auto&& user) {
+ onValidatorFinished(user);
+ };
}
- login.insert(QString("id"), QJsonValue(m_UUID));
- login.insert(QString("token"), QJsonValue(m_AuthToken));
- login.insert(QString("protocol"), 2);
- QJsonDocument loginDoc(login);
- QString finalMessage(loginDoc.toJson());
- m_nexusLogin->sendTextMessage(finalMessage);
- QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=%1&application=%2").arg(m_UUID).arg("modorganizer2")));
- m_loginTimer.start(30000);
+
+ addNexusLog(tr("Checking API key..."));
+ m_nexusValidator->start(key);
}
-void SettingsDialog::loginPing()
+void SettingsDialog::onSSOKeyChanged(const QString& key)
{
- if (m_nexusLogin->isValid()) {
- m_nexusLogin->ping();
- m_totalPings++;
- }
- if (m_totalPings >= 60) {
- m_loginTimer.stop();
- m_totalPings = 0;
- m_nexusLogin->close(QWebSocketProtocol::CloseCodeGoingAway, "Timeout: No response received after thirty minutes. Cancelling request.");
+ if (key.isEmpty()) {
+ clearKey();
+ } else {
+ addNexusLog(tr("Received API key."));
+ validateKey(key);
}
}
-void SettingsDialog::authError(QAbstractSocket::SocketError error)
+void SettingsDialog::onSSOStateChanged(NexusSSOLogin::States s, const QString& e)
{
- auto errorInfo = m_nexusLogin->errorString();
- qCritical() << "An error occurred: " << errorInfo;
+ if (s != NexusSSOLogin::Finished) {
+ // finished state is handled in onSSOKeyChanged()
+ const auto log = NexusSSOLogin::stateToString(s, e);
+
+ for (auto&& line : log.split("\n")) {
+ addNexusLog(line);
+ }
+ }
+
+ updateNexusState();
}
-void SettingsDialog::receiveApiKey(const QString &response)
+void SettingsDialog::onValidatorStateChanged(
+ NexusKeyValidator::States s, const QString& e)
{
- QJsonDocument responseDoc = QJsonDocument::fromJson(response.toUtf8());
- QVariantMap responseData = responseDoc.object().toVariantMap();
- if (responseData["success"].toBool()) {
- QVariantMap data = responseData["data"].toMap();
- if (data.contains("connection_token")) {
- m_AuthToken = data["connection_token"].toString();
- } else {
- const auto key = data["api_key"].toString();
-
- m_nexusLogin->close();
- m_loginTimer.stop();
- m_totalPings = 0;
+ if (s != NexusKeyValidator::Finished) {
+ // finished state is handled in onValidatorFinished()
+ const auto log = NexusKeyValidator::stateToString(s, e);
- if (key.isEmpty()) {
- clearKey();
- } else {
- setKey(key);
- }
+ for (auto&& line : log.split("\n")) {
+ addNexusLog(line);
}
- } else {
- QString error("There was a problem with SSO initialization: %1");
- qCritical() << error.arg(responseData["error"].toString());
- m_nexusLogin->close();
}
+
+ updateNexusState();
}
-void SettingsDialog::completeApiConnection()
+void SettingsDialog::onValidatorFinished(const APIUserAccount& user)
{
- if (!m_KeyReceived && !m_loginTimer.isActive()) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"),
- tr("Failed to retrieve a Nexus API key! Please try again. "
- "A browser window should open asking you to authorize."));
+ NexusInterface::instance(m_PluginContainer)->setUserAccount(user);
- // try again
- fetchNexusApiKey();
+ if (!user.apiKey().isEmpty()) {
+ if (setKey(user.apiKey())) {
+ addNexusLog(tr("Linked with Nexus successfully."));
+ }
}
}
+void SettingsDialog::addNexusLog(const QString& s)
+{
+ ui->nexusLog->addItem(s);
+ ui->nexusLog->scrollToBottom();
+}
+
bool SettingsDialog::setKey(const QString& key)
{
- m_KeyReceived = true;
+ m_keyChanged = true;
const bool ret = m_settings->setNexusApiKey(key);
- updateNexusButtons();
+ updateNexusState();
return ret;
}
bool SettingsDialog::clearKey()
{
- m_KeyCleared = true;
+ m_keyChanged = true;
const auto ret = m_settings->clearNexusApiKey();
- updateNexusButtons();
NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey();
+ updateNexusState();
return ret;
}
-void SettingsDialog::testApiKey()
+void SettingsDialog::updateNexusState()
{
- QString key;
- if (!m_settings->getNexusApiKey(key)) {
- qWarning().nospace() << "can't test API key, nothing stored";
- return;
- }
-
- NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(key, true);
+ updateNexusButtons();
+ updateNexusData();
}
void SettingsDialog::updateNexusButtons()
{
- if (m_nexusLogin->state() != QAbstractSocket::UnconnectedState) {
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
// api key is in the process of being retrieved
- ui->nexusConnect->setText("Connecting the API. Please login within the browser and accept the request. This will time out after 30 minutes.");
- ui->nexusConnect->setEnabled(false);
+ ui->nexusConnect->setText(tr("Cancel"));
+ ui->nexusConnect->setEnabled(true);
ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(false);
}
+ else if (m_nexusValidator && m_nexusValidator->isActive()) {
+ // api key is in the process of being tested
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
+ ui->nexusConnect->setEnabled(false);
+ ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Cancel"));
+ ui->nexusManualKey->setEnabled(true);
+ }
else if (m_settings->hasNexusApiKey()) {
// api key is present
- ui->nexusConnect->setText("Nexus API Key Stored");
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
ui->nexusConnect->setEnabled(false);
ui->nexusDisconnect->setEnabled(true);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(false);
} else {
// api key not present
- ui->nexusConnect->setText("Connect to Nexus");
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
ui->nexusConnect->setEnabled(true);
ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(true);
}
}
+void SettingsDialog::updateNexusData()
+{
+ const auto user = NexusInterface::instance(m_PluginContainer)
+ ->getAPIUserAccount();
+
+ if (user.isValid()) {
+ ui->nexusUserID->setText(user.id());
+ ui->nexusName->setText(user.name());
+ ui->nexusAccount->setText(localizedUserAccountType(user.type()));
+
+ ui->nexusDailyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingDailyRequests)
+ .arg(user.limits().maxDailyRequests));
+
+ ui->nexusHourlyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingHourlyRequests)
+ .arg(user.limits().maxHourlyRequests));
+ } else {
+ ui->nexusUserID->setText(tr("N/A"));
+ ui->nexusName->setText(tr("N/A"));
+ ui->nexusAccount->setText(tr("N/A"));
+ ui->nexusDailyRequests->setText(tr("N/A"));
+ ui->nexusHourlyRequests->setText(tr("N/A"));
+ }
+}
+
void SettingsDialog::storeSettings(QListWidgetItem *pluginItem)
{
if (pluginItem != nullptr) {
@@ -619,11 +660,6 @@ void SettingsDialog::on_clearCacheButton_clicked()
NexusInterface::instance(m_PluginContainer)->clearCache();
}
-void SettingsDialog::on_nexusDisconnect_clicked()
-{
- clearKey();
-}
-
void SettingsDialog::normalizePath(QLineEdit *lineEdit)
{
QString text = lineEdit->text();
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index 858a36d4..c5f487fd 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -21,12 +21,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define SETTINGSDIALOG_H
#include "tutorabledialog.h"
+#include "nxmaccessmanager.h"
#include <iplugin.h>
-#include <QComboBox>
-#include <QDialog>
-#include <QWebSocket>
#include <QListWidgetItem>
-#include <QTimer>
class PluginContainer;
class Settings;
@@ -35,6 +32,7 @@ namespace Ui {
class SettingsDialog;
}
+
/**
* dialog used to change settings for Mod Organizer. On top of the
* settings managed by the "Settings" class, this offers a button to open the
@@ -127,11 +125,6 @@ private slots:
void on_resetGeometryBtn_clicked();
void deleteBlacklistItem();
- void dispatchLogin();
- void loginPing();
- void authError(QAbstractSocket::SocketError error);
- void receiveApiKey(const QString &apiKey);
- void completeApiConnection();
private:
Ui::SettingsDialog *ui;
@@ -145,23 +138,28 @@ private:
QColor m_ContainsColor;
QColor m_ContainedColor;
- bool m_KeyReceived;
- bool m_KeyCleared;
bool m_GeometriesReset;
- QString m_UUID;
- QString m_AuthToken;
+ bool m_keyChanged;
QString m_ExecutableBlacklist;
- QWebSocket *m_nexusLogin;
- QTimer m_loginTimer;
- int m_totalPings = 0;
+ std::unique_ptr<NexusSSOLogin> m_nexusLogin;
+ std::unique_ptr<NexusKeyValidator> m_nexusValidator;
+ void validateKey(const QString& key);
bool setKey(const QString& key);
bool clearKey();
+
+ void updateNexusState();
void updateNexusButtons();
+ void updateNexusData();
+
+ void onSSOKeyChanged(const QString& key);
+ void onSSOStateChanged(NexusSSOLogin::States s, const QString& e);
+
+ void onValidatorStateChanged(NexusKeyValidator::States s, const QString& e);
+ void onValidatorFinished(const APIUserAccount& user);
- void fetchNexusApiKey();
- void testApiKey();
+ void addNexusLog(const QString& s);
};
#endif // SETTINGSDIALOG_H
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index faaf1653..fccc8be0 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -451,112 +451,237 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
<attribute name="title">
<string>Nexus</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
+ <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1,0,0,0,1">
<item>
- <widget class="QGroupBox" name="nexusBox">
- <property name="toolTip">
- <string>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;quot; will open a Nexus webpage to authorise Mod Organizer. You will need to be logged into your Nexus account. The authorisation is stored in the Windows Credential Manager. Your Nexus username and password are not required or stored by Mod Organizer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Nexus Account</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>User ID:</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusUserID">
+ <property name="text">
+ <string>id</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusName">
+ <property name="text">
+ <string>name</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="nexusAccount">
+ <property name="text">
+ <string>account</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Statistics</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_3">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_30">
+ <property name="text">
+ <string>Daily requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusDailyRequests">
+ <property name="text">
+ <string>daily requests</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_31">
+ <property name="text">
+ <string>Hourly requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusHourlyRequests">
+ <property name="text">
+ <string>hourly requests</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
<property name="title">
- <string>Nexus</string>
+ <string>Nexus Connection</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_10">
- <item>
- <widget class="QPushButton" name="nexusConnect">
- <property name="text">
- <string>Connect to Nexus</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="nexusConnect">
+ <property name="text">
+ <string>Connect to Nexus</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusManualKey">
+ <property name="toolTip">
+ <string>Manually enter the API key and try to login</string>
+ </property>
+ <property name="text">
+ <string>Enter API Key Manually</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusDisconnect">
+ <property name="toolTip">
+ <string>Clear the stored Nexus API key and force reauthorization.</string>
+ </property>
+ <property name="text">
+ <string>Disconnect from Nexus</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <spacer name="horizontalSpacer_7">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="nexusManualKey">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="toolTip">
- <string>Manually enter the API key and try to login</string>
- </property>
- <property name="text">
- <string>Enter API Key Manually</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="nexusDisconnect">
- <property name="toolTip">
- <string>Clear the stored Nexus API key and force reauthorization.</string>
- </property>
- <property name="text">
- <string>Disconnect from Nexus</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="clearCacheButton">
- <property name="toolTip">
- <string>Remove cache and cookies.</string>
- </property>
- <property name="text">
- <string>Clear Cache</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QListWidget" name="nexusLog">
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
</layout>
</widget>
</item>
<item>
+ <widget class="QWidget" name="widget_4" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ <item>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QCheckBox" name="offlineBox">
@@ -619,6 +744,20 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
+ <widget class="QPushButton" name="clearCacheButton">
+ <property name="toolTip">
+ <string>Remove cache and cookies.</string>
+ </property>
+ <property name="text">
+ <string>Clear Cache</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -682,19 +821,6 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</item>
- <item>
- <spacer name="verticalSpacer_3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
</layout>
</widget>
<widget class="QWidget" name="steamTab">
@@ -1359,7 +1485,6 @@ programs you are intentionally running.</string>
<tabstop>browseProfilesDirBtn</tabstop>
<tabstop>overwriteDirEdit</tabstop>
<tabstop>browseOverwriteDirBtn</tabstop>
- <tabstop>clearCacheButton</tabstop>
<tabstop>associateButton</tabstop>
<tabstop>knownServersList</tabstop>
<tabstop>preferredServersList</tabstop>
diff --git a/src/statusbar.cpp b/src/statusbar.cpp
index 8ad5bea1..e9a6e658 100644
--- a/src/statusbar.cpp
+++ b/src/statusbar.cpp
@@ -23,7 +23,7 @@ StatusBar::StatusBar(QStatusBar* bar, Ui::MainWindow* ui) :
m_bar->addPermanentWidget(m_notifications);
m_bar->addPermanentWidget(m_update);
m_bar->addPermanentWidget(m_api);
-
+
m_progress->setTextVisible(true);
m_progress->setRange(0, 100);
@@ -72,8 +72,8 @@ void StatusBar::setAPI(const APIStats& stats, const APIUserAccount& user)
if (user.type() == APIUserAccountTypes::None) {
text = "API: not logged in";
- textColor = "initial";
- backgroundColor = "transparent";
+ textColor = "";
+ backgroundColor = "";
} else {
text = QString("API: Queued: %1 | Daily: %2 | Hourly: %3")
.arg(stats.requestsQueued)
@@ -94,20 +94,25 @@ void StatusBar::setAPI(const APIStats& stats, const APIUserAccount& user)
m_api->setText(text);
- m_api->setStyleSheet(QString(R"(
+ QString ss(R"(
QLabel
{
padding-left: 0.1em;
padding-right: 0.1em;
padding-top: 0;
- padding-bottom: 0;
- color: %1;
- background-color: %2;
- }
- )")
- .arg(textColor)
- .arg(backgroundColor));
+ padding-bottom: 0;)");
+
+ if (!textColor.isEmpty()) {
+ ss += QString("\ncolor: %1;").arg(textColor);
+ }
+
+ if (!backgroundColor.isEmpty()) {
+ ss += QString("\nbackground-color: %2;").arg(backgroundColor);
+ }
+
+ ss += "\n}";
+ m_api->setStyleSheet(ss);
m_api->setAutoFillBackground(true);
}
diff --git a/src/version.rc b/src/version.rc
index 04259d7d..92370853 100644
--- a/src/version.rc
+++ b/src/version.rc
@@ -4,7 +4,7 @@
// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser
// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha
#define VER_FILEVERSION 2,2,1
-#define VER_FILEVERSION_STR "2.2.1alpha1\0"
+#define VER_FILEVERSION_STR "2.2.1rc1\0"
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_FILEVERSION