summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2019-06-15 13:56:34 -0400
committerGitHub <noreply@github.com>2019-06-15 13:56:34 -0400
commit14c9da193919b367cae48659fc0102b16cdc71a7 (patch)
treee9e1121c92d8bcaad6a9f17451e0532bf52792d4
parent52aa6ff68105b9cae6842578fca417187dc43196 (diff)
parenteed0f1503d2b2bf03d1ad823c72f7d279a1b41f6 (diff)
Merge pull request #762 from isanae/statusbar
-rw-r--r--src/CMakeLists.txt6
-rw-r--r--src/apiuseraccount.cpp67
-rw-r--r--src/apiuseraccount.h129
-rw-r--r--src/mainwindow.cpp183
-rw-r--r--src/mainwindow.h18
-rw-r--r--src/mainwindow.ui55
-rw-r--r--src/nexusinterface.cpp254
-rw-r--r--src/nexusinterface.h31
-rw-r--r--src/nxmaccessmanager.cpp26
-rw-r--r--src/nxmaccessmanager.h8
-rw-r--r--src/settingsdialog.cpp6
-rw-r--r--src/statusbar.cpp152
-rw-r--r--src/statusbar.h51
13 files changed, 718 insertions, 268 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 93597d62..5623a851 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -116,6 +116,8 @@ SET(organizer_SRCS
forcedloaddialog.cpp
forcedloaddialogwidget.cpp
filterwidget.cpp
+ statusbar.cpp
+ apiuseraccount.cpp
shared/windows_error.cpp
shared/error_report.cpp
@@ -213,6 +215,8 @@ SET(organizer_HDRS
forcedloaddialog.h
forcedloaddialogwidget.h
filterwidget.h
+ statusbar.h
+ apiuseraccount.h
shared/windows_error.h
shared/error_report.h
@@ -277,6 +281,7 @@ set(application
moshortcut
selfupdater
singleinstance
+ statusbar
)
set(browser
@@ -296,6 +301,7 @@ set(core
nxmaccessmanager
organizercore
organizerproxy
+ apiuseraccount
)
set(dialogs
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp
new file mode 100644
index 00000000..b901e41a
--- /dev/null
+++ b/src/apiuseraccount.cpp
@@ -0,0 +1,67 @@
+#include "apiuseraccount.h"
+
+APIUserAccount::APIUserAccount()
+ : m_type(APIUserAccountTypes::None)
+{
+}
+
+const QString& APIUserAccount::id() const
+{
+ return m_id;
+}
+
+const QString& APIUserAccount::name() const
+{
+ return m_name;
+}
+
+APIUserAccountTypes APIUserAccount::type() const
+{
+ return m_type;
+}
+
+const APILimits& APIUserAccount::limits() const
+{
+ return m_limits;
+}
+
+APIUserAccount& APIUserAccount::id(const QString& id)
+{
+ m_id = id;
+ return *this;
+}
+
+APIUserAccount& APIUserAccount::name(const QString& name)
+{
+ m_name = name;
+ return *this;
+}
+
+APIUserAccount& APIUserAccount::type(APIUserAccountTypes type)
+{
+ m_type = type;
+ return *this;
+}
+
+APIUserAccount& APIUserAccount::limits(const APILimits& limits)
+{
+ m_limits = limits;
+ return *this;
+}
+
+int APIUserAccount::remainingRequests() const
+{
+ return std::max(
+ m_limits.remainingDailyRequests,
+ m_limits.remainingHourlyRequests);
+}
+
+bool APIUserAccount::shouldThrottle() const
+{
+ return (remainingRequests() < ThrottleThreshold);
+}
+
+bool APIUserAccount::exhausted() const
+{
+ return (remainingRequests() <= 0);
+}
diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h
new file mode 100644
index 00000000..8a238d71
--- /dev/null
+++ b/src/apiuseraccount.h
@@ -0,0 +1,129 @@
+#ifndef APIUSERACCOUNT_H
+#define APIUSERACCOUNT_H
+
+#include <QString>
+
+/**
+* represents user account types on a mod provider website such as nexus
+*/
+enum class APIUserAccountTypes
+{
+ // not logged in
+ None = 0,
+
+ // regular account
+ Regular,
+
+ // premium account
+ Premium
+};
+
+
+/**
+* current limits imposed on the user account
+**/
+struct APILimits
+{
+ // maximum number of requests per day
+ int maxDailyRequests = 0;
+
+ // remaining number of requests today
+ int remainingDailyRequests = 0;
+
+ // maximum number of requests per hour
+ int maxHourlyRequests = 0;
+
+ // remaining number of requests this hour
+ int remainingHourlyRequests = 0;
+};
+
+
+/**
+* API statistics
+*/
+struct APIStats
+{
+ // number of API requests currently queued
+ int requestsQueued = 0;
+};
+
+
+/**
+* represents a user account on the mod provier website
+*/
+class APIUserAccount
+{
+public:
+ // when the number of remanining requests is under this number, further
+ // requests will be throttled by avoiding non-critical ones
+ static const int ThrottleThreshold = 200;
+
+ APIUserAccount();
+
+ /**
+ * user id
+ */
+ const QString& id() const;
+
+ /**
+ * user name
+ */
+ const QString& name() const;
+
+ /**
+ * account type
+ */
+ APIUserAccountTypes type() const;
+
+ /**
+ * current API limits
+ */
+ const APILimits& limits() const;
+
+
+ /**
+ * sets the user id
+ */
+ APIUserAccount& id(const QString& id);
+
+ /**
+ * sets the user name
+ **/
+ APIUserAccount& name(const QString& name);
+
+ /**
+ * sets the acount type
+ */
+ APIUserAccount& type(APIUserAccountTypes type);
+
+ /**
+ * sets the current limits
+ */
+ APIUserAccount& limits(const APILimits& limits);
+
+
+ /**
+ * returns the number of remaining requests
+ */
+ int remainingRequests() const;
+
+ /**
+ * whether the number of remaining requests is low enough that further
+ * requests should be throttled
+ */
+ bool shouldThrottle() const;
+
+ /**
+ * true if all the remaining requests have been used and the API will refuse
+ * further requests
+ */
+ bool exhausted() const;
+
+private:
+ QString m_id, m_name;
+ APIUserAccountTypes m_type;
+ APILimits m_limits;
+ APIStats m_stats;
+};
+
+#endif // APIUSERACCOUNT_H
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 911d0ff1..205ab7cc 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -77,6 +77,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "nxmaccessmanager.h"
#include "appconfig.h"
#include "eventfilter.h"
+#include "statusbar.h"
#include <utility.h>
#include <dataarchives.h>
#include <bsainvalidation.h>
@@ -202,6 +203,7 @@ MainWindow::MainWindow(QSettings &initSettings
, ui(new Ui::MainWindow)
, m_WasVisible(false)
, m_menuBarVisible(true)
+ , m_statusBarVisible(true)
, m_linksSeparator(nullptr)
, m_Tutorial(this, "MainWindow")
, m_OldProfileIndex(-1)
@@ -223,8 +225,34 @@ MainWindow::MainWindow(QSettings &initSettings
QWebEngineProfile::defaultProfile()->setHttpCacheMaximumSize(52428800);
QWebEngineProfile::defaultProfile()->setCachePath(m_OrganizerCore.settings().getCacheDirectory());
QWebEngineProfile::defaultProfile()->setPersistentStoragePath(m_OrganizerCore.settings().getCacheDirectory());
+
ui->setupUi(this);
- updateWindowTitle(QString(), 0, false);
+ m_statusBar.reset(new StatusBar(statusBar(), ui));
+
+ {
+ auto* ni = NexusInterface::instance(&m_PluginContainer);
+
+ // there are two ways to get here:
+ // 1) the user just started MO, and
+ // 2) the user has changed some setting that required a restart
+ //
+ // "restarting" MO doesn't actually re-execute the binary, it just basically
+ // executes most of main() again, so a bunch of things are actually not
+ // reset
+ //
+ // one of these things is the api status, which will have fired its events
+ // long before the execution gets here because stuff is still cached and no
+ // real request to nexus is actually done
+ //
+ // therefore, when the user starts MO normally, the user account and stats
+ // will be empty (which is fine) and populated later on when the api key
+ // check has finished
+ //
+ // in the rare case where the user restarts MO through the settings, this
+ // will correctly pick up the previous values
+ updateWindowTitle(ni->getAPIUserAccount());
+ m_statusBar->setAPI(ni->getAPIStats(), ni->getAPIUserAccount());
+ }
languageChange(m_OrganizerCore.settings().language());
@@ -242,14 +270,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
ui->logList, SLOT(scrollToBottom()));
- m_RefreshProgress = new QProgressBar(statusBar());
- m_RefreshProgress->setTextVisible(true);
- m_RefreshProgress->setRange(0, 100);
- m_RefreshProgress->setValue(0);
- m_RefreshProgress->setVisible(false);
- statusBar()->addWidget(m_RefreshProgress, 1000);
- statusBar()->clearMessage();
-
updateProblemsButton();
setupToolbar();
@@ -345,13 +365,6 @@ MainWindow::MainWindow(QSettings &initSettings
ui->bossButton->setToolTip(tr("There is no supported sort mechanism for this game. You will probably have to use a third-party tool."));
}
- ui->apiRequests->setAutoFillBackground(true);
- QPalette palette = ui->apiRequests->palette();
- palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen);
- palette.setColor(ui->apiRequests->foregroundRole(), Qt::white);
- ui->apiRequests->setPalette(palette);
- ui->apiRequests->setVisible(!m_OrganizerCore.settings().hideAPICounter());
-
connect(&m_PluginContainer, SIGNAL(diagnosisUpdate()), this, SLOT(updateProblemsButton()));
connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), this, SLOT(saveSelectionChanged(QListWidgetItem*)));
@@ -384,11 +397,24 @@ MainWindow::MainWindow(QSettings &initSettings
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(), SIGNAL(credentialsReceived(const QString&, int, bool, std::tuple<int, int, int, int>)),
- this, SLOT(updateWindowTitle(const QString&, int, bool)));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(credentialsReceived(const QString&, int, bool, std::tuple<int, int, int, int>)),
- NexusInterface::instance(&m_PluginContainer), SLOT(setRateMax(const QString&, int, bool, std::tuple<int, int, int, int>)));
- connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestsChanged(int, std::tuple<int, int, int, int>)), this, SLOT(updateAPICounter(int, std::tuple<int, int, int, int>)));
+
+ connect(
+ NexusInterface::instance(&pluginContainer)->getAccessManager(),
+ SIGNAL(credentialsReceived(const APIUserAccount&)),
+ this,
+ SLOT(updateWindowTitle(const APIUserAccount&)));
+
+ connect(
+ NexusInterface::instance(&pluginContainer)->getAccessManager(),
+ SIGNAL(credentialsReceived(const APIUserAccount&)),
+ NexusInterface::instance(&m_PluginContainer),
+ SLOT(setUserAccount(const APIUserAccount&)));
+
+ connect(
+ NexusInterface::instance(&pluginContainer),
+ SIGNAL(requestsChanged(const APIStats&, const APIUserAccount&)),
+ this,
+ SLOT(onRequestsChanged(const APIStats&, const APIUserAccount&)));
connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString)));
connect(ui->tabWidget, SIGNAL(currentChanged(int)), &TutorialManager::instance(), SIGNAL(tabChanged(int)));
@@ -553,20 +579,27 @@ MainWindow::~MainWindow()
}
-void MainWindow::updateWindowTitle(const QString &accountName, int, bool premium)
+void MainWindow::updateWindowTitle(const APIUserAccount& user)
{
QString title = QString("%1 Mod Organizer v%2").arg(
m_OrganizerCore.managedGame()->gameName(),
m_OrganizerCore.getVersion().displayString(3));
- if (!accountName.isEmpty()) {
- title.append(QString(" (%1%2)").arg(accountName, premium ? "*" : ""));
+ if (!user.name().isEmpty()) {
+ const QString premium = (user.type() == APIUserAccountTypes::Premium ? "*" : "");
+ title.append(QString(" (%1%2)").arg(user.name(), premium));
}
this->setWindowTitle(title);
}
+void MainWindow::onRequestsChanged(const APIStats& stats, const APIUserAccount& user)
+{
+ m_statusBar->setAPI(stats, user);
+}
+
+
void MainWindow::disconnectPlugins()
{
if (ui->actionTool->menu() != nullptr) {
@@ -711,6 +744,7 @@ void MainWindow::updatePinnedExecutables()
iconForExecutable(iter->m_BinaryInfo.filePath()), iter->m_Title);
exeAction->setObjectName(QString("custom__") + iter->m_Title);
+ exeAction->setStatusTip(iter->m_BinaryInfo.filePath());
if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) {
qDebug("failed to connect trigger?");
@@ -744,6 +778,7 @@ void MainWindow::toolbarMenu_aboutToShow()
ui->actionMainMenuToggle->setChecked(ui->menuBar->isVisible());
ui->actionToolBarMainToggle->setChecked(ui->toolBar->isVisible());
+ ui->actionStatusBarToggle->setChecked(ui->statusBar->isVisible());
ui->actionToolBarSmallIcons->setChecked(ui->toolBar->iconSize() == SmallToolbarSize);
ui->actionToolBarMediumIcons->setChecked(ui->toolBar->iconSize() == MediumToolbarSize);
@@ -761,8 +796,7 @@ QMenu* MainWindow::createPopupMenu()
void MainWindow::on_actionMainMenuToggle_triggered()
{
- ui->menuBar->setVisible(!ui->menuBar->isVisible());
- m_menuBarVisible = ui->menuBar->isVisible();
+ showMenuBar(!ui->menuBar->isVisible());
}
void MainWindow::on_actionToolBarMainToggle_triggered()
@@ -770,6 +804,11 @@ void MainWindow::on_actionToolBarMainToggle_triggered()
ui->toolBar->setVisible(!ui->toolBar->isVisible());
}
+void MainWindow::on_actionStatusBarToggle_triggered()
+{
+ showStatusBar(!ui->statusBar->isVisible());
+}
+
void MainWindow::on_actionToolBarSmallIcons_triggered()
{
setToolbarSize(SmallToolbarSize);
@@ -814,6 +853,36 @@ void MainWindow::setToolbarButtonStyle(Qt::ToolButtonStyle s)
}
}
+void MainWindow::showMenuBar(bool b)
+{
+ ui->menuBar->setVisible(b);
+ m_menuBarVisible = b;
+}
+
+void MainWindow::showStatusBar(bool b)
+{
+ ui->statusBar->setVisible(b);
+ m_statusBarVisible = b;
+
+ // the central widget typically has no bottom padding because the status bar
+ // is more than enough, but when it's hidden, the bottom widget (currently
+ // the log) touches the bottom border of the window, which looks ugly
+ //
+ // when hiding the statusbar, the central widget is given the same border
+ // margin as it has on the top (which is typically 6, as it's the default from
+ // the qt designer)
+
+ auto m = ui->centralWidget->layout()->contentsMargins();
+
+ if (b) {
+ m.setBottom(0);
+ } else {
+ m.setBottom(m.top());
+ }
+
+ ui->centralWidget->layout()->setContentsMargins(m);
+}
+
void MainWindow::on_centralWidget_customContextMenuRequested(const QPoint &pos)
{
// this allows for getting the context menu even if both the menubar and all
@@ -882,6 +951,8 @@ void MainWindow::updateProblemsButton()
final = original;
}
+ ui->actionNotifications->setEnabled(numProblems > 0);
+
// setting the icon on the action (shown on the menu)
ui->actionNotifications->setIcon(final);
@@ -891,6 +962,11 @@ void MainWindow::updateProblemsButton()
button->setIcon(final);
}
}
+
+ // updating the status bar, may be null very early when MO is starting
+ if (m_statusBar) {
+ m_statusBar->setNotifications(numProblems > 0);
+ }
}
@@ -2083,8 +2159,11 @@ void MainWindow::readSettings()
}
if (settings.contains("menubar_visible")) {
- m_menuBarVisible = settings.value("menubar_visible").toBool();
- ui->menuBar->setVisible(m_menuBarVisible);
+ showMenuBar(settings.value("menubar_visible").toBool());
+ }
+
+ if (settings.contains("statusbar_visible")) {
+ showStatusBar(settings.value("statusbar_visible").toBool());
}
if (settings.contains("window_split")) {
@@ -2180,6 +2259,7 @@ void MainWindow::storeSettings(QSettings &settings) {
settings.setValue("toolbar_size", ui->toolBar->iconSize());
settings.setValue("toolbar_button_style", static_cast<int>(ui->toolBar->toolButtonStyle()));
settings.setValue("menubar_visible", m_menuBarVisible);
+ settings.setValue("statusbar_visible", m_statusBarVisible);
settings.setValue("window_split", ui->splitter->saveState());
settings.setValue("window_monitor", QApplication::desktop()->screenNumber(this));
settings.setValue("log_split", ui->topLevelSplitter->saveState());
@@ -2534,15 +2614,8 @@ void MainWindow::setESPListSorting(int index)
void MainWindow::refresher_progress(int percent)
{
- if (percent == 100) {
- m_RefreshProgress->setVisible(false);
- this->setEnabled(true);
- } else if (!m_RefreshProgress->isVisible()) {
- this->setEnabled(false);
- m_RefreshProgress->setVisible(true);
- m_RefreshProgress->setRange(0, 100);
- m_RefreshProgress->setValue(percent);
- }
+ setEnabled(percent == 100);
+ m_statusBar->setProgress(percent);
}
void MainWindow::directory_refreshed()
@@ -5279,8 +5352,7 @@ void MainWindow::on_actionSettings_triggered()
activateProxy(settings.useProxy());
}
- ui->apiRequests->setVisible(!settings.hideAPICounter());
-
+ m_statusBar->checkSettings(m_OrganizerCore.settings());
updateDownloadView();
m_OrganizerCore.updateVFSParams(settings.logLevel(), settings.crashDumpsType(), settings.executablesBlacklist());
@@ -5588,13 +5660,9 @@ void MainWindow::openDataOriginExplorer_clicked()
void MainWindow::updateAvailable()
{
- for (QAction *action : ui->toolBar->actions()) {
- if (action->text() == tr("Update")) {
- action->setEnabled(true);
- action->setToolTip(tr("Update available"));
- break;
- }
- }
+ ui->actionUpdate->setEnabled(true);
+ ui->actionUpdate->setToolTip(tr("Update available"));
+ m_statusBar->setUpdateAvailable(true);
}
@@ -6079,26 +6147,6 @@ void MainWindow::nxmRequestFailed(QString gameName, int modID, int, QVariant, in
}
-void MainWindow::updateAPICounter(int queueCount, std::tuple<int, int, int, int> limits)
-{
- ui->apiRequests->setText(QString("API: Q: %1 | D: %2 | H: %3").arg(queueCount).arg(std::get<0>(limits)).arg(std::get<2>(limits)));
- int requestsRemaining = std::get<0>(limits) + std::get<2>(limits);
- if (requestsRemaining > 300) {
- QPalette palette = ui->apiRequests->palette();
- palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkGreen);
- ui->apiRequests->setPalette(palette);
- } else if (requestsRemaining < 150) {
- QPalette palette = ui->apiRequests->palette();
- palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkRed);
- ui->apiRequests->setPalette(palette);
- } else {
- QPalette palette = ui->apiRequests->palette();
- palette.setColor(ui->apiRequests->backgroundRole(), Qt::darkYellow);
- ui->apiRequests->setPalette(palette);
- }
-}
-
-
BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination,
QProgressDialog &progress)
{
@@ -7008,8 +7056,7 @@ void MainWindow::keyReleaseEvent(QKeyEvent *event)
// if the menubar is hidden, pressing Alt will make it visible
if (event->key() == Qt::Key_Alt) {
if (!ui->menuBar->isVisible()) {
- ui->menuBar->setVisible(true);
- m_menuBarVisible = true;
+ showMenuBar(true);
}
}
diff --git a/src/mainwindow.h b/src/mainwindow.h
index b8d9f49f..88389738 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -37,6 +37,7 @@ struct Executable;
class CategoryFactory;
class LockedDialogBase;
class OrganizerCore;
+class StatusBar;
#include "plugincontainer.h" //class PluginContainer;
class PluginListSortProxy;
namespace BSA { class Archive; }
@@ -75,7 +76,6 @@ class QListWidgetItem;
class QMenu;
class QModelIndex;
class QPoint;
-class QProgressBar;
class QProgressDialog;
class QTranslator;
class QTreeWidgetItem;
@@ -326,8 +326,10 @@ private:
bool m_WasVisible;
// this has to be remembered because by the time storeSettings() is called,
- // the window is closed and the menubar is hidden
- bool m_menuBarVisible;
+ // the window is closed and the all bars are hidden
+ bool m_menuBarVisible, m_statusBarVisible;
+
+ std::unique_ptr<StatusBar> m_statusBar;
// last separator on the toolbar, used to add spacer for right-alignment and
// as an insert point for executables
@@ -338,7 +340,6 @@ private:
int m_OldProfileIndex;
std::vector<QString> m_ModNameList; // the mod-list to go with the directory structure
- QProgressBar *m_RefreshProgress;
bool m_Refreshing;
QStringList m_DefaultArchives;
@@ -415,8 +416,7 @@ private:
private slots:
- void updateWindowTitle(const QString &accountName, int, bool premium);
-
+ void updateWindowTitle(const APIUserAccount& user);
void showMessage(const QString &message);
void showError(const QString &message);
@@ -543,7 +543,7 @@ private slots:
void nxmDownloadURLs(QString, int modID, int fileID, QVariant userData, QVariant resultData, int requestID);
void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString);
- void updateAPICounter(int queueCount, std::tuple<int, int, int, int> limits);
+ void onRequestsChanged(const APIStats& stats, const APIUserAccount& user);
void editCategories();
void deselectFilters();
@@ -651,6 +651,7 @@ private slots: // ui slots
void on_actionExit_triggered();
void on_actionMainMenuToggle_triggered();
void on_actionToolBarMainToggle_triggered();
+ void on_actionStatusBarToggle_triggered();
void on_actionToolBarSmallIcons_triggered();
void on_actionToolBarMediumIcons_triggered();
void on_actionToolBarLargeIcons_triggered();
@@ -694,6 +695,9 @@ private slots: // ui slots
void on_categoriesAndBtn_toggled(bool checked);
void on_categoriesOrBtn_toggled(bool checked);
void on_managedArchiveLabel_linkHovered(const QString &link);
+
+ void showMenuBar(bool b);
+ void showStatusBar(bool b);
};
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index d3f9ef39..98743b7e 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -412,7 +412,7 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,0,1,1,1">
+ <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,0,1,1">
<item>
<widget class="QPushButton" name="displayCategoriesBtn">
<property name="maximumSize">
@@ -471,37 +471,6 @@ p, li { white-space: pre-wrap; }
</property>
</spacer>
</item>
- <item>
- <widget class="QLabel" name="apiRequests">
- <property name="toolTip">
- <string>Nexus API Queued and Remaining Requests</string>
- </property>
- <property name="whatsThis">
- <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This tracks the number of queued Nexus API requests on the left (&lt;span style=&quot; font-weight:600;&quot;&gt;Q&lt;/span&gt;) and the remaining daily (&lt;span style=&quot; font-weight:600;&quot;&gt;D&lt;/span&gt;) and hourly (&lt;span style=&quot; font-weight:600;&quot;&gt;H&lt;/span&gt;) requests on the right. The Nexus API limits you to a pool of requests per day and requests per hour. It is dynamically updated every time a request is completed. If you run out of requests, you will be unable to queue downloads, check updates, parse mod info, or even log in. Both pools must be consumed before this happens.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
- <property name="frameShape">
- <enum>QFrame::StyledPanel</enum>
- </property>
- <property name="frameShadow">
- <enum>QFrame::Sunken</enum>
- </property>
- <property name="lineWidth">
- <number>2</number>
- </property>
- <property name="midLineWidth">
- <number>1</number>
- </property>
- <property name="text">
- <string>API: Q: 0 | D: 0 | H: 0</string>
- </property>
- <property name="alignment">
- <set>Qt::AlignCenter</set>
- </property>
- <property name="margin">
- <number>2</number>
- </property>
- </widget>
- </item>
<item alignment="Qt::AlignLeft">
<widget class="QPushButton" name="clearFiltersButton">
<property name="sizePolicy">
@@ -547,19 +516,6 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
- <spacer name="horizontalSpacer_4">
- <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="QComboBox" name="groupCombo">
<property name="baseSize">
<size>
@@ -1441,6 +1397,7 @@ p, li { white-space: pre-wrap; }
</property>
<addaction name="actionMainMenuToggle"/>
<addaction name="actionToolBarMainToggle"/>
+ <addaction name="actionStatusBarToggle"/>
<addaction name="separator"/>
<addaction name="actionToolBarSmallIcons"/>
<addaction name="actionToolBarMediumIcons"/>
@@ -1776,6 +1733,14 @@ p, li { white-space: pre-wrap; }
<string>&amp;Menu</string>
</property>
</action>
+ <action name="actionStatusBarToggle">
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="text">
+ <string>St&amp;atus bar</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index c677add0..ee9acf2c 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -38,6 +38,17 @@ using namespace MOBase;
using namespace MOShared;
+void throttledWarning(const APIUserAccount& user)
+{
+ qCritical() <<
+ QString(
+ "You have fewer than %1 requests remaining (%2). Only downloads and "
+ "login validation are being allowed.")
+ .arg(APIUserAccount::ThrottleThreshold)
+ .arg(user.remainingRequests());
+}
+
+
NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule)
: m_Interface(NexusInterface::instance(pluginContainer))
, m_SubModule(subModule)
@@ -179,15 +190,39 @@ void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVar
QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0);
+APILimits NexusInterface::defaultAPILimits()
+{
+ // https://app.swaggerhub.com/apis-docs/NexusMods/nexus-mods_public_api_params_in_form_data/1.0#/
+ const int MaxDaily = 2500;
+ const int MaxHourly = 100;
+
+ APILimits limits;
+
+ limits.maxDailyRequests = MaxDaily;
+ limits.remainingDailyRequests = MaxDaily;
+ limits.maxHourlyRequests = MaxHourly;
+ limits.remainingHourlyRequests = MaxHourly;
+
+ return limits;
+}
+
+APILimits NexusInterface::parseLimits(const QNetworkReply* reply)
+{
+ 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();
+
+ return limits;
+}
+
+
NexusInterface::NexusInterface(PluginContainer *pluginContainer)
: m_PluginContainer(pluginContainer)
- , m_RemainingDailyRequests(2500)
- , m_RemainingHourlyRequests(100)
- , m_MaxDailyRequests(2500)
- , m_MaxHourlyRequests(100)
- , m_IsPremium(false)
- , m_UserID(0)
{
+ m_User.limits(defaultAPILimits());
m_MOVersion = createVersionInfo();
m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString(3));
@@ -222,15 +257,10 @@ void NexusInterface::loginCompleted()
nextRequest();
}
-void NexusInterface::setRateMax(const QString&, int userId, bool isPremium, std::tuple<int, int, int, int> limits)
+void NexusInterface::setUserAccount(const APIUserAccount& user)
{
- m_RemainingDailyRequests = std::get<0>(limits);
- m_MaxDailyRequests = std::get<1>(limits);
- m_RemainingHourlyRequests = std::get<2>(limits);
- m_MaxHourlyRequests = std::get<3>(limits);
- m_IsPremium = isPremium;
- m_UserID = userId;
- emit requestsChanged(m_RequestQueue.size(), limits);
+ m_User = user;
+ emit requestsChanged(getAPIStats(), m_User);
}
void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query)
@@ -369,70 +399,70 @@ int NexusInterface::requestDescription(QString gameName, int modID, QObject *rec
int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) {
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
+ if (m_User.shouldThrottle()) {
+ throttledWarning(m_User);
+ return -1;
+ }
- connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+ connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
- nextRequest();
- return requestInfo.m_ID;
- }
- qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.")
- .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests));
- return -1;
+ connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
+ receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+
+ nextRequest();
+ return requestInfo.m_ID;
}
int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData,
const QString &subModule, const MOBase::IPluginGame *game)
{
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) {
- NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
+ if (m_User.shouldThrottle()) {
+ throttledWarning(m_User);
+ return -1;
+ }
- connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)),
- receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
+ NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+ connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)),
+ receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
- nextRequest();
- return requestInfo.m_ID;
- }
- qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.")
- .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests));
- return -1;
+ connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
+ receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+
+ nextRequest();
+ return requestInfo.m_ID;
}
int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData,
QString gameName, const QString &subModule)
{
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) {
- IPluginGame *game = getGame(gameName);
- if (game == nullptr) {
- qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName));
- return -1;
- }
+ if (m_User.shouldThrottle()) {
+ throttledWarning(m_User);
+ return -1;
+ }
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game);
- m_RequestQueue.enqueue(requestInfo);
+ IPluginGame *game = getGame(gameName);
+ if (game == nullptr) {
+ qCritical("requestUpdates can't find plugin for %s", qUtf8Printable(gameName));
+ return -1;
+ }
- connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game);
+ m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+ connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
- nextRequest();
- return requestInfo.m_ID;
- }
- qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.")
- .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests));
- return -1;
+ connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
+ receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+
+ nextRequest();
+ return requestInfo.m_ID;
}
@@ -527,23 +557,23 @@ int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData,
int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) {
- NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
- requestInfo.m_Endorse = endorse;
- m_RequestQueue.enqueue(requestInfo);
+ if (m_User.shouldThrottle()) {
+ throttledWarning(m_User);
+ return -1;
+ }
- connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
- receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
+ NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
+ requestInfo.m_Endorse = endorse;
+ m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+ connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
+ receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
- nextRequest();
- return requestInfo.m_ID;
- }
- qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.")
- .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests));
- return -1;
+ connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
+ receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+
+ nextRequest();
+ return requestInfo.m_ID;
}
int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule)
@@ -564,23 +594,23 @@ int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, co
int NexusInterface::requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) >= 200) {
- NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game);
- requestInfo.m_Track = track;
- m_RequestQueue.enqueue(requestInfo);
+ if (m_User.shouldThrottle()) {
+ throttledWarning(m_User);
+ return -1;
+ }
- connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)),
- receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection);
+ NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game);
+ requestInfo.m_Track = track;
+ m_RequestQueue.enqueue(requestInfo);
- connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
- receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+ connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)),
+ receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection);
- nextRequest();
- return requestInfo.m_ID;
- }
- qCritical() << QString("You have fewer than 200 requests remaining (%1). Only downloads and login validation are being allowed.")
- .arg(std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests));
- return -1;
+ connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
+ receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
+
+ nextRequest();
+ return requestInfo.m_ID;
}
int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData,
@@ -645,7 +675,7 @@ void NexusInterface::nextRequest()
}
}
- if (std::max(m_RemainingDailyRequests, m_RemainingHourlyRequests) <= 0) {
+ if (m_User.exhausted()) {
m_RequestQueue.clear();
QTime time = QTime::currentTime();
QTime targetTime;
@@ -696,9 +726,9 @@ void NexusInterface::nextRequest()
} break;
case NXMRequestInfo::TYPE_DOWNLOADURL: {
ModRepositoryFileInfo *fileInfo = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(info.m_UserData));
- if (m_IsPremium) {
+ if (m_User.type() == APIUserAccountTypes::Premium) {
url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID);
- } else if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires && fileInfo->nexusDownloadUser == m_UserID) {
+ } else if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires && fileInfo->nexusDownloadUser == m_User.id().toInt()) {
url = QString("%1/games/%2/mods/%3/files/%4/download_link?key=%5&expires=%6")
.arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID).arg(fileInfo->nexusKey).arg(fileInfo->nexusExpires);
} else {
@@ -780,23 +810,16 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
if (iter->m_AllowedErrors.contains(error) && iter->m_AllowedErrors[error].contains(statusCode)) {
// These errors are allows to silently happen. They should be handled in nxmRequestFailed below.
} else if (statusCode == 429) {
- m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt();
- m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt();
- m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt();
- m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt();
+ m_User.limits(parseLimits(reply));
- if (m_RemainingDailyRequests || m_RemainingHourlyRequests)
+ if (!m_User.exhausted()) {
qWarning("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team.");
- else
+ }
+ else {
qWarning("All API requests have been consumed and are now being denied.");
+ }
- emit requestsChanged(m_RequestQueue.size(), std::tuple<int, int, int, int>(std::make_tuple(
- m_RemainingDailyRequests,
- m_MaxDailyRequests,
- m_RemainingHourlyRequests,
- m_MaxHourlyRequests
- )));
-
+ emit requestsChanged(getAPIStats(), m_User);
qWarning("Error: %s", reply->errorString().toUtf8().constData());
} else {
qWarning("request failed: %s", reply->errorString().toUtf8().constData());
@@ -871,17 +894,8 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
} break;
}
- m_RemainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt();
- m_MaxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt();
- m_RemainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt();
- m_MaxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt();
-
- emit requestsChanged(m_RequestQueue.size(), std::tuple<int, int, int, int>(std::make_tuple(
- m_RemainingDailyRequests,
- m_MaxDailyRequests,
- m_RemainingHourlyRequests,
- m_MaxHourlyRequests
- )));
+ m_User.limits(parseLimits(reply));
+ emit requestsChanged(getAPIStats(), m_User);
} else {
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response"));
}
@@ -938,6 +952,20 @@ void NexusInterface::requestTimeout()
}
}
+APIUserAccount NexusInterface::getAPIUserAccount() const
+{
+ return m_User;
+}
+
+APIStats NexusInterface::getAPIStats() const
+{
+ APIStats stats;
+ stats.requestsQueued = m_RequestQueue.size();
+
+ return stats;
+}
+
+
namespace {
QString get_management_url()
{
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index 70c1c9c9..6e768149 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -20,6 +20,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#ifndef NEXUSINTERFACE_H
#define NEXUSINTERFACE_H
+#include "apiuseraccount.h"
+
#include <utility.h>
#include <versioninfo.h>
#include <imodrepositorybridge.h>
@@ -39,6 +41,7 @@ namespace MOBase { class IPluginGame; }
class NexusInterface;
class NXMAccessManager;
+
/**
* @brief convenience class to make nxm requests easier
* usually, all objects that started a nxm request will be signaled if one finished.
@@ -53,7 +56,6 @@ class NexusBridge : public MOBase::IModRepositoryBridge
Q_OBJECT
public:
-
NexusBridge(PluginContainer *pluginContainer, const QString &subModule = "");
/**
@@ -147,6 +149,8 @@ public:
};
public:
+ static APILimits defaultAPILimits();
+ static APILimits parseLimits(const QNetworkReply* reply);
~NexusInterface();
@@ -380,7 +384,7 @@ public:
/**
*
*/
- int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule,
+ int requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData, const QString &subModule,
MOBase::IPluginGame const *game);
/**
@@ -395,6 +399,9 @@ public:
std::vector<std::pair<QString, QString>> getGameChoices(const MOBase::IPluginGame *game);
+ APIUserAccount getAPIUserAccount() const;
+ APIStats getAPIStats() const;
+
public:
/**
@@ -459,11 +466,11 @@ signals:
void nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID);
void nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID);
void nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorString);
- void requestsChanged(int queueCount, std::tuple<int,int,int,int> requestsRemaining);
+ void requestsChanged(const APIStats& stats, const APIUserAccount& user);
public slots:
- void setRateMax(const QString&, int userId, bool isPremium, std::tuple<int,int,int,int> limits);
+ void setUserAccount(const APIUserAccount& user);
private slots:
@@ -534,27 +541,13 @@ private:
QString getOldModsURL(QString gameName) const;
private:
-
QNetworkDiskCache *m_DiskCache;
-
NXMAccessManager *m_AccessManager;
-
std::list<NXMRequestInfo> m_ActiveRequest;
QQueue<NXMRequestInfo> m_RequestQueue;
-
MOBase::VersionInfo m_MOVersion;
-
PluginContainer *m_PluginContainer;
-
- int m_RemainingDailyRequests;
- int m_RemainingHourlyRequests;
- int m_MaxDailyRequests;
- int m_MaxHourlyRequests;
-
- int m_UserID;
-
- bool m_IsPremium;
-
+ APIUserAccount m_User;
};
#endif // NEXUSINTERFACE_H
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index 684a53fa..ee6c03f1 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -18,8 +18,8 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "nxmaccessmanager.h"
-
#include "iplugingame.h"
+#include "nexusinterface.h"
#include "nxmurl.h"
#include "report.h"
#include "utility.h"
@@ -190,6 +190,7 @@ void NXMAccessManager::apiCheck(const QString &apiKey, bool force)
emit validateSuccessful(false);
return;
}
+
m_ApiKey = apiKey;
startValidationCheck();
}
@@ -218,6 +219,13 @@ QString NXMAccessManager::apiKey() const
return m_ApiKey;
}
+void NXMAccessManager::clearApiKey()
+{
+ m_ApiKey = "";
+ m_ValidateState = VALIDATE_NOT_VALID;
+
+ emit credentialsReceived(APIUserAccount());
+}
void NXMAccessManager::validateTimeout()
{
@@ -233,7 +241,6 @@ void NXMAccessManager::validateTimeout()
if (m_ValidateReply != nullptr) {
m_ValidateReply->deleteLater();
m_ValidateReply = nullptr;
- m_ValidateAttempted = false; // this usually means we might have success later
}
emit validateFailed(tr("There was a timeout during the request"));
@@ -280,20 +287,21 @@ void NXMAccessManager::validateFinished()
QString name = credentialsData.value("name").toString();
bool premium = credentialsData.value("is_premium").toBool();
- std::tuple<int, int, int, int> limits(std::make_tuple(
- m_ValidateReply->rawHeader("x-rl-daily-remaining").toInt(),
- m_ValidateReply->rawHeader("x-rl-daily-limit").toInt(),
- m_ValidateReply->rawHeader("x-rl-hourly-remaining").toInt(),
- m_ValidateReply->rawHeader("x-rl-hourly-limit").toInt()
- ));
+ const auto user = APIUserAccount()
+ .id(QString("%1").arg(id))
+ .name(name)
+ .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
+ .limits(NexusInterface::parseLimits(m_ValidateReply));
- emit credentialsReceived(name, id, premium, limits);
+
+ 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;
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index e3608f8d..1bdeae40 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -20,7 +20,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#ifndef NXMACCESSMANAGER_H
#define NXMACCESSMANAGER_H
-
+#include "apiuseraccount.h"
#include <QNetworkAccessManager>
#include <QTimer>
#include <QNetworkReply>
@@ -55,6 +55,7 @@ public:
QString userAgent(const QString &subModule = QString()) const;
QString apiKey() const;
+ void clearApiKey();
void startValidationCheck();
@@ -78,7 +79,7 @@ signals:
void validateFailed(const QString &message);
- void credentialsReceived(const QString &userName, int userId, bool premium, std::tuple<int, int, int, int> limits);
+ void credentialsReceived(const APIUserAccount& user);
private slots:
@@ -93,7 +94,6 @@ protected:
QIODevice *device);
private:
-
QTimer m_ValidateTimeout;
QNetworkReply *m_ValidateReply;
QProgressDialog *m_ProgressDialog { nullptr };
@@ -102,7 +102,6 @@ private:
QString m_ApiKey;
- bool m_ValidateAttempted;
enum {
VALIDATE_NOT_CHECKED,
VALIDATE_CHECKING,
@@ -111,7 +110,6 @@ private:
VALIDATE_REFUSED,
VALIDATE_VALID
} m_ValidateState = VALIDATE_NOT_CHECKED;
-
};
#endif // NXMACCESSMANAGER_H
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index b3e8c8a7..95e4ceb0 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -511,6 +511,9 @@ bool SettingsDialog::clearKey()
m_KeyCleared = true;
const auto ret = m_settings->clearNexusApiKey();
updateNexusButtons();
+
+ NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey();
+
return ret;
}
@@ -522,8 +525,7 @@ void SettingsDialog::testApiKey()
return;
}
- auto* am = NexusInterface::instance(m_PluginContainer)->getAccessManager();
- am->apiCheck(key, true);
+ NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(key, true);
}
void SettingsDialog::updateNexusButtons()
diff --git a/src/statusbar.cpp b/src/statusbar.cpp
new file mode 100644
index 00000000..01449202
--- /dev/null
+++ b/src/statusbar.cpp
@@ -0,0 +1,152 @@
+#include "statusbar.h"
+#include "nexusinterface.h"
+#include "settings.h"
+#include "ui_mainwindow.h"
+
+StatusBar::StatusBar(QStatusBar* bar, Ui::MainWindow* ui) :
+ m_bar(bar), m_progress(new QProgressBar),
+ m_notifications(new StatusBarAction(ui->actionNotifications)),
+ m_update(new StatusBarAction(ui->actionUpdate)),
+ m_api(new QLabel)
+{
+ m_bar->addPermanentWidget(m_progress);
+ m_bar->addPermanentWidget(m_notifications);
+ m_bar->addPermanentWidget(m_update);
+ m_bar->addPermanentWidget(m_api);
+
+ m_progress->setTextVisible(true);
+ m_progress->setRange(0, 100);
+ m_progress->setMaximumWidth(150);
+ m_progress->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
+
+ m_update->set(false);
+ m_notifications->set(false);
+
+ m_api->setObjectName("apistats");
+ m_api->setToolTip(QObject::tr(
+ "This tracks the number of queued Nexus API requests, as well as the "
+ "remaining daily and hourly requests. The Nexus API limits you to a pool "
+ "of requests per day and requests per hour. It is dynamically updated "
+ "every time a request is completed. If you run out of requests, you will "
+ "be unable to queue downloads, check updates, parse mod info, or even log "
+ "in. Both pools must be consumed before this happens."));
+
+ m_bar->clearMessage();
+ setProgress(-1);
+ setAPI({}, {});
+}
+
+void StatusBar::setProgress(int percent)
+{
+ if (percent < 0 || percent >= 100) {
+ m_bar->clearMessage();
+ m_progress->setVisible(false);
+ } else {
+ m_bar->showMessage(QObject::tr("Loading..."));
+ m_progress->setVisible(true);
+ m_progress->setValue(percent);
+ }
+}
+
+void StatusBar::setNotifications(bool hasNotifications)
+{
+ m_notifications->set(hasNotifications);
+}
+
+void StatusBar::setAPI(const APIStats& stats, const APIUserAccount& user)
+{
+ QString text;
+ QString textColor;
+ QString backgroundColor;
+
+ if (user.type() == APIUserAccountTypes::None) {
+ text = "API: not logged in";
+ textColor = "initial";
+ backgroundColor = "transparent";
+ } else {
+ text = QString("API: Queued: %1 | Daily: %2 | Hourly: %3")
+ .arg(stats.requestsQueued)
+ .arg(user.limits().remainingDailyRequests)
+ .arg(user.limits().remainingHourlyRequests);
+
+ if (user.remainingRequests() > 500) {
+ textColor = "white";
+ backgroundColor = "darkgreen";
+ } else if (user.remainingRequests() > 200) {
+ textColor = "black";
+ backgroundColor = "rgb(226, 192, 0)"; // yellow
+ } else {
+ textColor = "white";
+ backgroundColor = "darkred";
+ }
+ }
+
+ m_api->setText(text);
+
+ m_api->setStyleSheet(QString(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));
+
+ m_api->setAutoFillBackground(true);
+}
+
+void StatusBar::setUpdateAvailable(bool b)
+{
+ m_update->set(b);
+}
+
+void StatusBar::checkSettings(const Settings& settings)
+{
+ m_api->setVisible(!settings.hideAPICounter());
+}
+
+
+StatusBarAction::StatusBarAction(QAction* action)
+ : m_action(action), m_icon(new QLabel), m_text(new QLabel)
+{
+ setLayout(new QHBoxLayout);
+ layout()->setContentsMargins(0, 0, 0, 0);
+ layout()->addWidget(m_icon);
+ layout()->addWidget(m_text);
+}
+
+void StatusBarAction::set(bool visible)
+{
+ if (visible) {
+ m_icon->setPixmap(m_action->icon().pixmap(16, 16));
+ m_text->setText(cleanupActionText(m_action->text()));
+ }
+
+ setVisible(visible);
+}
+
+void StatusBarAction::mouseDoubleClickEvent(QMouseEvent* e)
+{
+ if (m_action->isEnabled()) {
+ m_action->trigger();
+ }
+}
+
+QString StatusBarAction::cleanupActionText(const QString& original) const
+{
+ QString s = original;
+
+ s.replace(QRegExp("\\&([^&])"), "\\1"); // &Item -> Item
+ s.replace("&&", "&"); // &&Item -> &Item
+
+ if (s.endsWith("...")) {
+ s = s.left(s.size() - 3);
+ }
+
+ return s;
+}
diff --git a/src/statusbar.h b/src/statusbar.h
new file mode 100644
index 00000000..2baf12ee
--- /dev/null
+++ b/src/statusbar.h
@@ -0,0 +1,51 @@
+#ifndef MO_STATUSBAR_H
+#define MO_STATUSBAR_H
+
+#include <QStatusBar>
+#include <QProgressBar>
+
+struct APIStats;
+class APIUserAccount;
+class Settings;
+namespace Ui { class MainWindow; }
+
+
+class StatusBarAction : public QWidget
+{
+public:
+ StatusBarAction(QAction* action);
+
+ void set(bool visible);
+
+protected:
+ void mouseDoubleClickEvent(QMouseEvent* e) override;
+
+private:
+ QAction* m_action;
+ QLabel* m_icon;
+ QLabel* m_text;
+
+ QString cleanupActionText(const QString& s) const;
+};
+
+
+class StatusBar
+{
+public:
+ StatusBar(QStatusBar* bar, Ui::MainWindow* ui);
+
+ void setProgress(int percent);
+ void setNotifications(bool hasNotifications);
+ void setAPI(const APIStats& stats, const APIUserAccount& user);
+ void setUpdateAvailable(bool b);
+ void checkSettings(const Settings& settings);
+
+private:
+ QStatusBar* m_bar;
+ QProgressBar* m_progress;
+ StatusBarAction* m_notifications;
+ StatusBarAction* m_update;
+ QLabel* m_api;
+};
+
+#endif // MO_STATUSBAR_H