Better client etiquette

This commit is contained in:
GriffinR 2024-01-24 11:56:04 -05:00
parent 34b2f9d881
commit a5ed554c68
9 changed files with 403 additions and 98 deletions

View file

@ -8,6 +8,8 @@
#include <QSize>
#include <QKeySequence>
#include <QMultiMap>
#include <QDateTime>
#include <QUrl>
#include "events.h"
@ -75,6 +77,8 @@ public:
this->projectSettingsTab = 0;
this->warpBehaviorWarningDisabled = false;
this->checkForUpdates = true;
this->lastUpdateCheckTime = QDateTime();
this->rateLimitTimes.clear();
}
void addRecentProject(QString project);
void setRecentProjects(QStringList projects);
@ -107,6 +111,8 @@ public:
void setProjectSettingsTab(int tab);
void setWarpBehaviorWarningDisabled(bool disabled);
void setCheckForUpdates(bool enabled);
void setLastUpdateCheckTime(QDateTime time);
void setRateLimitTimes(QMap<QUrl, QDateTime> map);
QString getRecentProject();
QStringList getRecentProjects();
bool getReopenOnLaunch();
@ -138,6 +144,8 @@ public:
int getProjectSettingsTab();
bool getWarpBehaviorWarningDisabled();
bool getCheckForUpdates();
QDateTime getLastUpdateCheckTime();
QMap<QUrl, QDateTime> getRateLimitTimes();
protected:
virtual QString getConfigFilepath() override;
virtual void parseConfigKeyValue(QString key, QString value) override;
@ -187,6 +195,8 @@ private:
int projectSettingsTab;
bool warpBehaviorWarningDisabled;
bool checkForUpdates;
QDateTime lastUpdateCheckTime;
QMap<QUrl, QDateTime> rateLimitTimes;
};
extern PorymapConfig porymapConfig;

87
include/core/network.h Normal file
View file

@ -0,0 +1,87 @@
#ifndef NETWORK_H
#define NETWORK_H
/*
The two classes defined here provide a simplified interface for Qt's network classes QNetworkAccessManager and QNetworkReply.
With the Qt classes, the workflow for a GET is roughly: generate a QNetworkRequest, give this request to QNetworkAccessManager::get,
connect the returned object to QNetworkReply::finished, and in the slot of that connection handle the various HTTP headers and attributes,
then manage errors or process the webpage's body.
These classes handle generating the QNetworkRequest with a given URL and manage the HTTP headers in the reply. They will automatically
respect rate limits and return cached data if the webpage hasn't changed since previous requests. Instead of interacting with a QNetworkReply,
callers interact with a simplified NetworkReplyData.
Example that logs Porymap's description on GitHub:
NetworkAccessManager * manager = new NetworkAccessManager(this);
NetworkReplyData * reply = manager->get("https://api.github.com/repos/huderlem/porymap");
connect(reply, &NetworkReplyData::finished, [reply] () {
if (!reply->errorString().isEmpty()) {
logError(QString("Failed to read description: %1").arg(reply->errorString()));
} else {
auto webpage = QJsonDocument::fromJson(reply->body());
logInfo(QString("Porymap: %1").arg(webpage["description"].toString()));
}
reply->deleteLater();
});
*/
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QDateTime>
class NetworkReplyData : public QObject
{
Q_OBJECT
public:
QUrl url() const { return m_url; }
QUrl nextUrl() const { return m_nextUrl; }
QByteArray body() const { return m_body; }
QString errorString() const { return m_error; }
QDateTime retryAfter() const { return m_retryAfter; }
bool isFinished() const { return m_finished; }
friend class NetworkAccessManager;
private:
QUrl m_url;
QUrl m_nextUrl;
QByteArray m_body;
QString m_error;
QDateTime m_retryAfter;
bool m_finished;
void finish() {
m_finished = true;
emit finished();
};
signals:
void finished();
};
class NetworkAccessManager : public QNetworkAccessManager
{
Q_OBJECT
public:
NetworkAccessManager(QObject * parent = nullptr);
~NetworkAccessManager();
NetworkReplyData * get(const QString &url);
NetworkReplyData * get(const QUrl &url);
private:
// For a more complex cache we could implement a QAbstractCache for the manager
struct CacheEntry {
QString eTag;
QByteArray data;
};
QMap<QUrl, CacheEntry*> cache;
QMap<QUrl, QDateTime> rateLimitTimes;
void processReply(QNetworkReply * reply, NetworkReplyData * data);
const QNetworkRequest getRequest(const QUrl &url);
};
#endif // NETWORK_H

View file

@ -12,7 +12,6 @@
#include <QCloseEvent>
#include <QAbstractItemModel>
#include <QJSValue>
#include <QNetworkAccessManager>
#include "project.h"
#include "orderedjson.h"
#include "config.h"
@ -311,7 +310,7 @@ private:
QPointer<ProjectSettingsEditor> projectSettingsEditor = nullptr;
QPointer<CustomScriptsEditor> customScriptsEditor = nullptr;
QPointer<UpdatePromoter> updatePromoter = nullptr;
QPointer<QNetworkAccessManager> networkAccessManager = nullptr;
QPointer<NetworkAccessManager> networkAccessManager = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
QStandardItemModel *mapListModel;
QList<QStandardItem*> *mapGroupItemsList;

View file

@ -1,10 +1,10 @@
#ifndef UPDATEPROMOTER_H
#define UPDATEPROMOTER_H
#include "network.h"
#include <QDialog>
#include <QPushButton>
#include <QNetworkAccessManager>
#include <QNetworkReply>
namespace Ui {
class UpdatePromoter;
@ -15,24 +15,30 @@ class UpdatePromoter : public QDialog
Q_OBJECT
public:
explicit UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager);
explicit UpdatePromoter(QWidget *parent, NetworkAccessManager *manager);
~UpdatePromoter() {};
void checkForUpdates();
void requestDialog();
void updatePreferences();
private:
Ui::UpdatePromoter *ui;
QNetworkAccessManager *const manager;
QNetworkReply * reply = nullptr;
NetworkAccessManager *const manager;
QPushButton * button_Downloads;
QString downloadLink;
QPushButton * button_Retry;
QString changelog;
QUrl downloadUrl;
bool breakingChanges;
bool foundReleases;
QSet<QUrl> visitedUrls; // Prevent infinite redirection
void resetDialog();
void processWebpage(const QJsonDocument &data);
void processError(const QString &err);
void get(const QUrl &url);
void processWebpage(const QJsonDocument &data, const QUrl &nextUrl);
void disableRequestsUntil(const QDateTime time);
void error(const QString &err);
bool isNewerVersion(int major, int minor, int patch);
private slots:

View file

@ -34,6 +34,7 @@ SOURCES += src/core/block.cpp \
src/core/mapparser.cpp \
src/core/metatile.cpp \
src/core/metatileparser.cpp \
src/core/network.cpp \
src/core/paletteutil.cpp \
src/core/parseutil.cpp \
src/core/tile.cpp \
@ -126,6 +127,7 @@ HEADERS += include/core/block.h \
include/core/mapparser.h \
include/core/metatile.h \
include/core/metatileparser.h \
include/core/network.h \
include/core/paletteutil.h \
include/core/parseutil.h \
include/core/tile.h \

View file

@ -409,6 +409,14 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
this->warpBehaviorWarningDisabled = getConfigBool(key, value);
} else if (key == "check_for_updates") {
this->checkForUpdates = getConfigBool(key, value);
} else if (key == "last_update_check_time") {
this->lastUpdateCheckTime = QDateTime::fromString(value).toLocalTime();
} else if (key.startsWith("rate_limit_time/")) {
static const QRegularExpression regex("\\brate_limit_time/(?<url>.+)");
QRegularExpressionMatch match = regex.match(key);
if (match.hasMatch()) {
this->rateLimitTimes.insert(match.captured("url"), QDateTime::fromString(value).toLocalTime());
}
} else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
}
@ -456,6 +464,13 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("project_settings_tab", QString::number(this->projectSettingsTab));
map.insert("warp_behavior_warning_disabled", QString::number(this->warpBehaviorWarningDisabled));
map.insert("check_for_updates", QString::number(this->checkForUpdates));
map.insert("last_update_check_time", this->lastUpdateCheckTime.toUTC().toString());
for (auto i = this->rateLimitTimes.cbegin(), end = this->rateLimitTimes.cend(); i != end; i++){
// Only include rate limit times that are still active (i.e., in the future)
const QDateTime time = i.value();
if (!time.isNull() && time > QDateTime::currentDateTime())
map.insert("rate_limit_time/" + i.key().toString(), time.toUTC().toString());
}
return map;
}
@ -634,6 +649,26 @@ void PorymapConfig::setProjectSettingsTab(int tab) {
this->save();
}
void PorymapConfig::setWarpBehaviorWarningDisabled(bool disabled) {
this->warpBehaviorWarningDisabled = disabled;
this->save();
}
void PorymapConfig::setCheckForUpdates(bool enabled) {
this->checkForUpdates = enabled;
this->save();
}
void PorymapConfig::setLastUpdateCheckTime(QDateTime time) {
this->lastUpdateCheckTime = time;
this->save();
}
void PorymapConfig::setRateLimitTimes(QMap<QUrl, QDateTime> map) {
this->rateLimitTimes = map;
this->save();
}
QString PorymapConfig::getRecentProject() {
return this->recentProjects.value(0);
}
@ -784,24 +819,22 @@ int PorymapConfig::getProjectSettingsTab() {
return this->projectSettingsTab;
}
void PorymapConfig::setWarpBehaviorWarningDisabled(bool disabled) {
this->warpBehaviorWarningDisabled = disabled;
this->save();
}
bool PorymapConfig::getWarpBehaviorWarningDisabled() {
return this->warpBehaviorWarningDisabled;
}
void PorymapConfig::setCheckForUpdates(bool enabled) {
this->checkForUpdates = enabled;
this->save();
}
bool PorymapConfig::getCheckForUpdates() {
return this->checkForUpdates;
}
QDateTime PorymapConfig::getLastUpdateCheckTime() {
return this->lastUpdateCheckTime;
}
QMap<QUrl, QDateTime> PorymapConfig::getRateLimitTimes() {
return this->rateLimitTimes;
}
const QStringList ProjectConfig::versionStrings = {
"pokeruby",
"pokefirered",

146
src/core/network.cpp Normal file
View file

@ -0,0 +1,146 @@
#include "network.h"
#include "config.h"
#include <QCoreApplication>
#include <QRegularExpression>
#include <QTimer>
// Fallback wait time (in seconds) for rate limiting
static const int DefaultWaitTime = 120;
NetworkAccessManager::NetworkAccessManager(QObject * parent) : QNetworkAccessManager(parent) {
// We store rate limit end times in the user's config so that Porymap will still respect them after a restart.
// To avoid reading/writing to a local file during network operations, we only read/write the file when the
// manager is created/destroyed respectively.
this->rateLimitTimes = porymapConfig.getRateLimitTimes();
this->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
};
NetworkAccessManager::~NetworkAccessManager() {
porymapConfig.setRateLimitTimes(this->rateLimitTimes);
qDeleteAll(this->cache);
}
const QNetworkRequest NetworkAccessManager::getRequest(const QUrl &url) {
QNetworkRequest request(url);
// Set User-Agent to porymap/#.#.#
request.setHeader(QNetworkRequest::UserAgentHeader, QString("%1/%2").arg(QCoreApplication::applicationName())
.arg(QCoreApplication::applicationVersion()));
// If we've made a successful request in this session already, set the If-None-Match header.
// We'll only get a full response from the server if the data has changed since this last request.
// This helps to avoid hitting rate limits.
auto cacheEntry = this->cache.value(url, nullptr);
if (cacheEntry)
request.setHeader(QNetworkRequest::IfNoneMatchHeader, cacheEntry->eTag);
return request;
}
NetworkReplyData * NetworkAccessManager::get(const QString &url) {
return this->get(QUrl(url));
}
NetworkReplyData * NetworkAccessManager::get(const QUrl &url) {
NetworkReplyData * data = new NetworkReplyData();
data->m_url = url;
// If we are rate-limited, don't send a new request.
if (this->rateLimitTimes.contains(url)) {
auto time = this->rateLimitTimes.value(url);
if (!time.isNull() && time > QDateTime::currentDateTime()) {
data->m_retryAfter = time;
data->m_error = QString("Rate limit reached. Please try again after %1.").arg(data->m_retryAfter.toString());
QTimer::singleShot(1000, data, &NetworkReplyData::finish); // We can't emit this signal before caller has a chance to connect
return data;
}
// Rate limiting expired
this->rateLimitTimes.remove(url);
}
QNetworkReply * reply = QNetworkAccessManager::get(this->getRequest(url));
connect(reply, &QNetworkReply::finished, [this, reply, data] {
this->processReply(reply, data);
data->finish();
});
return data;
}
void NetworkAccessManager::processReply(QNetworkReply * reply, NetworkReplyData * data) {
if (!reply || !reply->isFinished())
return;
auto url = reply->url();
reply->deleteLater();
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
// Handle pagination (specifically, the format GitHub uses).
// This header is still sent for a 304, so we don't need to bother caching it.
if (reply->hasRawHeader("link")) {
static const QRegularExpression regex("<(?<url>.+)?>; rel=\"next\"");
QRegularExpressionMatch match = regex.match(QString(reply->rawHeader("link")));
if (match.hasMatch())
data->m_nextUrl = QUrl(match.captured("url"));
}
if (statusCode == 304) {
// "Not Modified", data hasn't changed since our last request.
data->m_body = this->cache.value(url)->data;
return;
}
// Handle standard rate limit header
if (reply->hasRawHeader("retry-after")) {
auto retryAfter = QVariant(reply->rawHeader("retry-after"));
if (retryAfter.canConvert<QDateTime>()) {
data->m_retryAfter = retryAfter.toDateTime().toLocalTime();
} else if (retryAfter.canConvert<int>()) {
data->m_retryAfter = QDateTime::currentDateTime().addSecs(retryAfter.toInt());
}
if (data->m_retryAfter.isNull() || data->m_retryAfter <= QDateTime::currentDateTime()) {
data->m_retryAfter = QDateTime::currentDateTime().addSecs(DefaultWaitTime);
}
if (statusCode == 429) {
data->m_error = "Too many requests. ";
} else if (statusCode == 503) {
data->m_error = "Service busy or unavailable. ";
}
data->m_error.append(QString("Please try again after %1.").arg(data->m_retryAfter.toString()));
this->rateLimitTimes.insert(url, data->m_retryAfter);
return;
}
// Handle GitHub's rate limit headers. As of writing this is (without authentication) 60 requests per IP address per hour.
bool ok;
int limitRemaining = reply->rawHeader("x-ratelimit-remaining").toInt(&ok);
if (ok && limitRemaining <= 0) {
auto limitReset = reply->rawHeader("x-ratelimit-reset").toLongLong(&ok);
data->m_retryAfter = ok ? QDateTime::fromSecsSinceEpoch(limitReset).toLocalTime()
: QDateTime::currentDateTime().addSecs(DefaultWaitTime);;
data->m_error = QString("Too many requests. Please try again after %1.").arg(data->m_retryAfter.toString());
this->rateLimitTimes.insert(url, data->m_retryAfter);
return;
}
// Handle remaining errors generically
auto error = reply->error();
if (error != QNetworkReply::NoError) {
data->m_error = reply->errorString();
return;
}
// Successful reply, we've received new data. Insert this data in the cache.
CacheEntry * cacheEntry = this->cache.value(url, nullptr);
if (!cacheEntry) {
cacheEntry = new CacheEntry;
this->cache.insert(url, cacheEntry);
}
auto eTagHeader = reply->header(QNetworkRequest::ETagHeader);
if (eTagHeader.isValid())
cacheEntry->eTag = eTagHeader.toString();
cacheEntry->data = data->m_body = reply->readAll();
}

View file

@ -59,6 +59,7 @@ MainWindow::MainWindow(QWidget *parent) :
ui->setupUi(this);
cleanupLargeLog();
logInfo(QString("Launching Porymap v%1").arg(PORYMAP_VERSION));
this->initWindow();
if (porymapConfig.getReopenOnLaunch() && this->openProject(porymapConfig.getRecentProject(), true))
@ -255,7 +256,7 @@ void MainWindow::on_actionCheck_for_Updates_triggered() {
void MainWindow::checkForUpdates(bool requestedByUser) {
if (!this->networkAccessManager)
this->networkAccessManager = new QNetworkAccessManager(this);
this->networkAccessManager = new NetworkAccessManager(this);
if (!this->updatePromoter) {
this->updatePromoter = new UpdatePromoter(this, this->networkAccessManager);
@ -265,9 +266,17 @@ void MainWindow::checkForUpdates(bool requestedByUser) {
});
}
if (requestedByUser)
this->updatePromoter->requestDialog();
if (requestedByUser) {
openSubWindow(this->updatePromoter);
} else {
// This is an automatic update check. Only run if we haven't done one in the last 5 minutes
QDateTime lastCheck = porymapConfig.getLastUpdateCheckTime();
if (lastCheck.addSecs(5*60) >= QDateTime::currentDateTime())
return;
}
this->updatePromoter->checkForUpdates();
porymapConfig.setLastUpdateCheckTime(QDateTime::currentDateTime());
}
void MainWindow::initEditor() {

View file

@ -3,13 +3,13 @@
#include "log.h"
#include "config.h"
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDesktopServices>
#include <QTimer>
UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
UpdatePromoter::UpdatePromoter(QWidget *parent, NetworkAccessManager *manager)
: QDialog(parent),
ui(new Ui::UpdatePromoter),
manager(manager)
@ -18,6 +18,7 @@ UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
// Set up "Do not alert me" check box
this->updatePreferences();
ui->checkBox_StopAlerts->setVisible(false);
connect(ui->checkBox_StopAlerts, &QCheckBox::stateChanged, [this](int state) {
bool enable = (state != Qt::Checked);
porymapConfig.setCheckForUpdates(enable);
@ -25,7 +26,9 @@ UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
});
// Set up button box
this->button_Retry = ui->buttonBox->button(QDialogButtonBox::Retry);
this->button_Downloads = ui->buttonBox->addButton("Go to Downloads...", QDialogButtonBox::ActionRole);
ui->buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &UpdatePromoter::dialogButtonClicked);
this->resetDialog();
@ -33,47 +36,55 @@ UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
void UpdatePromoter::resetDialog() {
this->button_Downloads->setEnabled(false);
ui->text_Changelog->setVisible(false);
ui->label_Warning->setVisible(false);
ui->label_Status->setText("Checking for updates...");
ui->label_Status->setText("");
this->changelog = QString();
this->downloadLink = QString();
this->downloadUrl = QString();
this->breakingChanges = false;
this->foundReleases = false;
this->visitedUrls.clear();
}
void UpdatePromoter::checkForUpdates() {
// Ignore request if one is still active.
if (this->reply && !this->reply->isFinished())
// If the Retry button is disabled, making requests is disabled
if (!this->button_Retry->isEnabled())
return;
this->resetDialog();
ui->buttonBox->button(QDialogButtonBox::Retry)->setEnabled(false);
// We could get ".../releases/latest" to retrieve less data, but this would run into problems if the
this->resetDialog();
this->button_Retry->setEnabled(false);
ui->label_Status->setText("Checking for updates...");
// We could use the URL ".../releases/latest" to retrieve less data, but this would run into problems if the
// most recent item on the releases page is not actually a new release (like the static windows build).
// By getting all releases we can also present a multi-version changelog of all changes since the host release.
static const QNetworkRequest request(QUrl("https://api.github.com/repos/huderlem/porymap/releases"));
this->reply = this->manager->get(request);
connect(this->reply, &QNetworkReply::finished, [this] {
ui->buttonBox->button(QDialogButtonBox::Retry)->setEnabled(true);
auto error = this->reply->error();
if (error == QNetworkReply::NoError) {
this->processWebpage(QJsonDocument::fromJson(this->reply->readAll()));
} else {
this->processError(this->reply->errorString());
static const QUrl url("https://api.github.com/repos/huderlem/porymap/releases");
this->get(url);
}
void UpdatePromoter::get(const QUrl &url) {
this->visitedUrls.insert(url);
auto reply = this->manager->get(url);
connect(reply, &NetworkReplyData::finished, [this, reply] () {
if (!reply->errorString().isEmpty()) {
this->error(reply->errorString());
this->disableRequestsUntil(reply->retryAfter());
} else {
this->processWebpage(QJsonDocument::fromJson(reply->body()), reply->nextUrl());
}
reply->deleteLater();
});
}
// Read all the items on the releases page, ignoring entries without a version identifier tag.
// Objects in the releases page data are sorted newest to oldest.
void UpdatePromoter::processWebpage(const QJsonDocument &data) {
bool updateAvailable = false;
bool breakingChanges = false;
bool foundRelease = false;
// Returns true when finished, returns false to request processing for the next page.
void UpdatePromoter::processWebpage(const QJsonDocument &data, const QUrl &nextUrl) {
const QJsonArray releases = data.array();
for (int i = 0; i < releases.size(); i++) {
int i;
for (i = 0; i < releases.size(); i++) {
auto release = releases.at(i).toObject();
// Convert tag string to version numbers
@ -89,57 +100,77 @@ void UpdatePromoter::processWebpage(const QJsonDocument &data) {
if (!ok) continue;
// We've found a valid release tag. If the version number is not newer than the host version then we can stop looking at releases.
foundRelease = true;
this->foundReleases = true;
if (!this->isNewerVersion(major, minor, patch))
break;
const QString description = release.value("body").toString();
const QString url = release.value("html_url").toString();
if (description.isEmpty() || url.isEmpty()) {
if (description.isEmpty()) {
// If the release was published very recently it won't have a description yet, in which case don't tell the user about it yet.
// If there's no URL, something has gone wrong and we should skip this release.
continue;
}
if (this->downloadLink.isEmpty()) {
if (this->downloadUrl.isEmpty()) {
// This is the first (newest) release we've found. Record its URL for download.
this->downloadLink = url;
breakingChanges = (major > PORYMAP_VERSION_MAJOR);
const QUrl url = QUrl(release.value("html_url").toString());
if (url.isEmpty()) {
// If there's no URL, something has gone wrong and we should skip this release.
continue;
}
this->downloadUrl = url;
this->breakingChanges = (major > PORYMAP_VERSION_MAJOR);
}
// Record the changelog of this release so we can show all changes since the host release.
this->changelog.append(QString("## %1\n%2\n\n").arg(tagName).arg(description));
updateAvailable = true;
}
if (!foundRelease) {
// We retrieved the webpage but didn't successfully parse any releases.
this->processError("Error parsing releases webpage");
// If we read the entire page then we didn't find a release as old as the host version.
// Keep looking on the second page, there might still be new releases there.
if (i == releases.size() && !nextUrl.isEmpty() && !this->visitedUrls.contains(nextUrl)) {
this->get(nextUrl);
return;
}
// If there's a new update available the dialog will always be opened.
// Otherwise the dialog is only open if the user requested it.
if (updateAvailable) {
this->button_Downloads->setEnabled(!this->downloadLink.isEmpty());
if (!this->foundReleases) {
// We retrieved the webpage but didn't successfully parse any releases.
this->error("Error parsing releases webpage");
return;
}
// Populate dialog with result
bool updateAvailable = !this->changelog.isEmpty();
ui->label_Status->setText(updateAvailable ? "A new version of Porymap is available!"
: "Your version of Porymap is up to date!");
ui->label_Warning->setVisible(this->breakingChanges);
ui->text_Changelog->setMarkdown(this->changelog);
ui->text_Changelog->setVisible(true);
ui->label_Warning->setVisible(breakingChanges);
ui->label_Status->setText("A new version of Porymap is available!");
ui->text_Changelog->setVisible(updateAvailable);
this->button_Downloads->setEnabled(!this->downloadUrl.isEmpty());
this->button_Retry->setEnabled(true);
// Alert the user if there's a new update available and the dialog wasn't already open.
// Show the window, but also show the option to turn off automatic alerts in the future.
if (updateAvailable && !this->isVisible()) {
ui->checkBox_StopAlerts->setVisible(true);
this->show();
} else {
// The rest of the UI remains in the state set by resetDialog
ui->label_Status->setText("Your version of Porymap is up to date!");
}
}
void UpdatePromoter::processError(const QString &err) {
const QString message = QString("Failed to check for version update: %1").arg(err);
if (this->isVisible()) {
ui->label_Status->setText(message);
} else {
logWarn(message);
void UpdatePromoter::disableRequestsUntil(const QDateTime time) {
this->button_Retry->setEnabled(false);
auto timeUntil = QDateTime::currentDateTime().msecsTo(time);
if (timeUntil < 0) timeUntil = 0;
QTimer::singleShot(timeUntil, Qt::VeryCoarseTimer, [this]() {
this->button_Retry->setEnabled(true);
});
}
void UpdatePromoter::error(const QString &err) {
const QString message = QString("Failed to check for version update: %1").arg(err);
ui->label_Status->setText(message);
if (!this->isVisible())
logWarn(message);
}
bool UpdatePromoter::isNewerVersion(int major, int minor, int patch) {
@ -150,35 +181,17 @@ bool UpdatePromoter::isNewerVersion(int major, int minor, int patch) {
return patch > PORYMAP_VERSION_PATCH;
}
// The dialog can either be shown programmatically when an update is available
// or if the user manually selects "Check for Updates" in the menu.
// When the dialog is shown programmatically there is a check box to disable automatic alerts.
// If the user requested the dialog (and it wasn't already open) this check box should be hidden.
void UpdatePromoter::requestDialog() {
if (!this->isVisible()){
ui->checkBox_StopAlerts->setVisible(false);
this->show();
} else if (this->isMinimized()) {
this->showNormal();
} else {
this->raise();
this->activateWindow();
}
}
void UpdatePromoter::updatePreferences() {
const QSignalBlocker blocker(ui->checkBox_StopAlerts);
ui->checkBox_StopAlerts->setChecked(porymapConfig.getCheckForUpdates());
ui->checkBox_StopAlerts->setChecked(!porymapConfig.getCheckForUpdates());
}
void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) {
auto buttonRole = ui->buttonBox->buttonRole(button);
if (buttonRole == QDialogButtonBox::RejectRole) {
if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::RejectRole) {
this->close();
} else if (buttonRole == QDialogButtonBox::AcceptRole) {
// "Retry" button
} else if (button == this->button_Retry) {
this->checkForUpdates();
} else if (button == this->button_Downloads && !this->downloadLink.isEmpty()) {
QDesktopServices::openUrl(QUrl(this->downloadLink));
} else if (button == this->button_Downloads) {
QDesktopServices::openUrl(this->downloadUrl);
}
}