Better client etiquette
This commit is contained in:
parent
34b2f9d881
commit
a5ed554c68
9 changed files with 403 additions and 98 deletions
|
@ -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
87
include/core/network.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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
146
src/core/network.cpp
Normal 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();
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue