Merge pull request #590 from GriffinRichards/update-promoter
Add update promoter
This commit is contained in:
commit
b228c4e76c
18 changed files with 782 additions and 50 deletions
|
@ -52,9 +52,6 @@
|
|||
<pointsize>12</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Version 5.3.0 - January 15th, 2024</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
|
|
|
@ -1715,7 +1715,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>100</width>
|
||||
<height>30</height>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -1809,7 +1809,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>100</width>
|
||||
<height>30</height>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -1903,7 +1903,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>100</width>
|
||||
<height>30</height>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -2003,7 +2003,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>100</width>
|
||||
<height>30</height>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -2097,7 +2097,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>100</width>
|
||||
<height>30</height>
|
||||
<height>16</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -3112,6 +3112,7 @@
|
|||
<addaction name="actionAbout_Porymap"/>
|
||||
<addaction name="actionOpen_Log_File"/>
|
||||
<addaction name="actionOpen_Config_Folder"/>
|
||||
<addaction name="actionCheck_for_Updates"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuOptions">
|
||||
<property name="title">
|
||||
|
@ -3406,6 +3407,14 @@
|
|||
<string>Custom Scripts...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCheck_for_Updates">
|
||||
<property name="text">
|
||||
<string>Check for Updates...</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::ApplicationSpecificRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<customwidgets>
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_MonitorProjectFiles">
|
||||
<property name="toolTip">
|
||||
<string>If checked, a prompt to reload your project will appear if relevant project files are edited</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Monitor project files</string>
|
||||
</property>
|
||||
|
@ -33,11 +36,24 @@
|
|||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_OpenRecentProject">
|
||||
<property name="toolTip">
|
||||
<string>If checked, Porymap will automatically open your most recently opened project on startup</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Open recent project on launch</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_CheckForUpdates">
|
||||
<property name="toolTip">
|
||||
<string>If checked, Porymap will automatically alert you on startup if a new release is available</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Automatically check for updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
104
forms/updatepromoter.ui
Normal file
104
forms/updatepromoter.ui
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>UpdatePromoter</class>
|
||||
<widget class="QDialog" name="UpdatePromoter">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>592</width>
|
||||
<height>484</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Porymap Version Update</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_Status">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_Warning">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><span style=" font-size:13pt; color:#d7000c;">WARNING: </span><span style=" font-weight:400;">Updating Porymap may require you to update your projects. See "Breaking Changes" in the Changelog for details.</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame_Changelog">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="text_Changelog">
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBox_StopAlerts">
|
||||
<property name="text">
|
||||
<string>Do not alert me about new updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Close|QDialogButtonBox::Retry</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
|
@ -8,9 +8,14 @@
|
|||
#include <QSize>
|
||||
#include <QKeySequence>
|
||||
#include <QMultiMap>
|
||||
#include <QDateTime>
|
||||
#include <QUrl>
|
||||
#include <QVersionNumber>
|
||||
|
||||
#include "events.h"
|
||||
|
||||
static const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION);
|
||||
|
||||
// In both versions the default new map border is a generic tree
|
||||
#define DEFAULT_BORDER_RSE (QList<uint16_t>{0x1D4, 0x1D5, 0x1DC, 0x1DD})
|
||||
#define DEFAULT_BORDER_FRLG (QList<uint16_t>{0x14, 0x15, 0x1C, 0x1D})
|
||||
|
@ -74,6 +79,10 @@ public:
|
|||
this->paletteEditorBitDepth = 24;
|
||||
this->projectSettingsTab = 0;
|
||||
this->warpBehaviorWarningDisabled = false;
|
||||
this->checkForUpdates = true;
|
||||
this->lastUpdateCheckTime = QDateTime();
|
||||
this->lastUpdateCheckVersion = porymapVersion;
|
||||
this->rateLimitTimes.clear();
|
||||
}
|
||||
void addRecentProject(QString project);
|
||||
void setRecentProjects(QStringList projects);
|
||||
|
@ -105,6 +114,10 @@ public:
|
|||
void setPaletteEditorBitDepth(int bitDepth);
|
||||
void setProjectSettingsTab(int tab);
|
||||
void setWarpBehaviorWarningDisabled(bool disabled);
|
||||
void setCheckForUpdates(bool enabled);
|
||||
void setLastUpdateCheckTime(QDateTime time);
|
||||
void setLastUpdateCheckVersion(QVersionNumber version);
|
||||
void setRateLimitTimes(QMap<QUrl, QDateTime> map);
|
||||
QString getRecentProject();
|
||||
QStringList getRecentProjects();
|
||||
bool getReopenOnLaunch();
|
||||
|
@ -135,6 +148,10 @@ public:
|
|||
int getPaletteEditorBitDepth();
|
||||
int getProjectSettingsTab();
|
||||
bool getWarpBehaviorWarningDisabled();
|
||||
bool getCheckForUpdates();
|
||||
QDateTime getLastUpdateCheckTime();
|
||||
QVersionNumber getLastUpdateCheckVersion();
|
||||
QMap<QUrl, QDateTime> getRateLimitTimes();
|
||||
protected:
|
||||
virtual QString getConfigFilepath() override;
|
||||
virtual void parseConfigKeyValue(QString key, QString value) override;
|
||||
|
@ -183,6 +200,10 @@ private:
|
|||
int paletteEditorBitDepth;
|
||||
int projectSettingsTab;
|
||||
bool warpBehaviorWarningDisabled;
|
||||
bool checkForUpdates;
|
||||
QDateTime lastUpdateCheckTime;
|
||||
QVersionNumber lastUpdateCheckVersion;
|
||||
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
|
|
@ -27,6 +27,7 @@
|
|||
#include "preferenceeditor.h"
|
||||
#include "projectsettingseditor.h"
|
||||
#include "customscriptseditor.h"
|
||||
#include "updatepromoter.h"
|
||||
|
||||
|
||||
|
||||
|
@ -288,6 +289,7 @@ private slots:
|
|||
void on_spinBox_SelectedCollision_valueChanged(int collision);
|
||||
void on_actionRegion_Map_Editor_triggered();
|
||||
void on_actionPreferences_triggered();
|
||||
void on_actionCheck_for_Updates_triggered();
|
||||
void togglePreferenceSpecificUi();
|
||||
void on_actionProject_Settings_triggered();
|
||||
void on_actionCustom_Scripts_triggered();
|
||||
|
@ -307,6 +309,8 @@ private:
|
|||
QPointer<PreferenceEditor> preferenceEditor = nullptr;
|
||||
QPointer<ProjectSettingsEditor> projectSettingsEditor = nullptr;
|
||||
QPointer<CustomScriptsEditor> customScriptsEditor = nullptr;
|
||||
QPointer<UpdatePromoter> updatePromoter = nullptr;
|
||||
QPointer<NetworkAccessManager> networkAccessManager = nullptr;
|
||||
FilterChildrenProxyModel *mapListProxyModel;
|
||||
QStandardItemModel *mapListModel;
|
||||
QList<QStandardItem*> *mapGroupItemsList;
|
||||
|
@ -397,6 +401,8 @@ private:
|
|||
QObjectList shortcutableObjects() const;
|
||||
void addCustomHeaderValue(QString key, QJsonValue value, bool isNew = false);
|
||||
int insertTilesetLabel(QStringList * list, QString label);
|
||||
|
||||
void checkForUpdates(bool requestedByUser);
|
||||
};
|
||||
|
||||
enum MapListUserRoles {
|
||||
|
|
|
@ -52,7 +52,6 @@ public:
|
|||
static QJSValue fromBlock(Block block);
|
||||
static QJSValue fromTile(Tile tile);
|
||||
static Tile toTile(QJSValue obj);
|
||||
static QJSValue version(QList<int> versionNums);
|
||||
static QJSValue dimensions(int width, int height);
|
||||
static QJSValue position(int x, int y);
|
||||
static const QImage * getImage(const QString &filepath, bool useCache);
|
||||
|
|
|
@ -14,7 +14,6 @@ class AboutPorymap : public QMainWindow
|
|||
public:
|
||||
explicit AboutPorymap(QWidget *parent = nullptr);
|
||||
~AboutPorymap();
|
||||
QList<int> getVersionNumbers();
|
||||
private:
|
||||
Ui::AboutPorymap *ui;
|
||||
};
|
||||
|
|
50
include/ui/updatepromoter.h
Normal file
50
include/ui/updatepromoter.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
#ifndef UPDATEPROMOTER_H
|
||||
#define UPDATEPROMOTER_H
|
||||
|
||||
#include "network.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPushButton>
|
||||
#include <QVersionNumber>
|
||||
|
||||
namespace Ui {
|
||||
class UpdatePromoter;
|
||||
}
|
||||
|
||||
class UpdatePromoter : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit UpdatePromoter(QWidget *parent, NetworkAccessManager *manager);
|
||||
~UpdatePromoter() {};
|
||||
|
||||
void checkForUpdates();
|
||||
void updatePreferences();
|
||||
|
||||
private:
|
||||
Ui::UpdatePromoter *ui;
|
||||
NetworkAccessManager *const manager;
|
||||
QPushButton * button_Downloads;
|
||||
QPushButton * button_Retry;
|
||||
|
||||
QString changelog;
|
||||
QUrl downloadUrl;
|
||||
QVersionNumber newVersion;
|
||||
bool foundReleases;
|
||||
|
||||
QSet<QUrl> visitedUrls; // Prevent infinite redirection
|
||||
|
||||
void resetDialog();
|
||||
void get(const QUrl &url);
|
||||
void processWebpage(const QJsonDocument &data, const QUrl &nextUrl);
|
||||
void error(const QString &err, const QDateTime time = QDateTime());
|
||||
|
||||
private slots:
|
||||
void dialogButtonClicked(QAbstractButton *button);
|
||||
|
||||
signals:
|
||||
void changedPreferences();
|
||||
};
|
||||
|
||||
#endif // UPDATEPROMOTER_H
|
15
porymap.pro
15
porymap.pro
|
@ -4,7 +4,7 @@
|
|||
#
|
||||
#-------------------------------------------------
|
||||
|
||||
QT += core gui qml
|
||||
QT += core gui qml network
|
||||
|
||||
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
|
||||
|
||||
|
@ -14,6 +14,8 @@ RC_ICONS = resources/icons/porymap-icon-2.ico
|
|||
ICON = resources/icons/porymap.icns
|
||||
QMAKE_CXXFLAGS += -std=c++17 -Wall
|
||||
QMAKE_TARGET_BUNDLE_PREFIX = com.pret
|
||||
VERSION = 5.3.0
|
||||
DEFINES += PORYMAP_VERSION=\\\"$$VERSION\\\"
|
||||
|
||||
SOURCES += src/core/block.cpp \
|
||||
src/core/bitpacker.cpp \
|
||||
|
@ -26,6 +28,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 \
|
||||
|
@ -102,7 +105,8 @@ SOURCES += src/core/block.cpp \
|
|||
src/project.cpp \
|
||||
src/settings.cpp \
|
||||
src/log.cpp \
|
||||
src/ui/uintspinbox.cpp
|
||||
src/ui/uintspinbox.cpp \
|
||||
src/ui/updatepromoter.cpp
|
||||
|
||||
HEADERS += include/core/block.h \
|
||||
include/core/bitpacker.h \
|
||||
|
@ -117,6 +121,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 \
|
||||
|
@ -196,7 +201,8 @@ HEADERS += include/core/block.h \
|
|||
include/scriptutility.h \
|
||||
include/settings.h \
|
||||
include/log.h \
|
||||
include/ui/uintspinbox.h
|
||||
include/ui/uintspinbox.h \
|
||||
include/ui/updatepromoter.h
|
||||
|
||||
FORMS += forms/mainwindow.ui \
|
||||
forms/prefabcreationdialog.ui \
|
||||
|
@ -214,7 +220,8 @@ FORMS += forms/mainwindow.ui \
|
|||
forms/colorpicker.ui \
|
||||
forms/projectsettingseditor.ui \
|
||||
forms/customscriptseditor.ui \
|
||||
forms/customscriptslistitem.ui
|
||||
forms/customscriptslistitem.ui \
|
||||
forms/updatepromoter.ui
|
||||
|
||||
RESOURCES += \
|
||||
resources/images.qrc \
|
||||
|
|
|
@ -407,6 +407,24 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
|
|||
this->projectSettingsTab = getConfigInteger(key, value, 0);
|
||||
} else if (key == "warp_behavior_warning_disabled") {
|
||||
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 == "last_update_check_version") {
|
||||
auto version = QVersionNumber::fromString(value);
|
||||
if (version.segmentCount() != 3) {
|
||||
logWarn(QString("Invalid config value for %1: '%2'. Must be 3 numbers separated by '.'").arg(key).arg(value));
|
||||
this->lastUpdateCheckVersion = porymapVersion;
|
||||
} else {
|
||||
this->lastUpdateCheckVersion = version;
|
||||
}
|
||||
} 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));
|
||||
}
|
||||
|
@ -453,6 +471,15 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
|
|||
map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth));
|
||||
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());
|
||||
map.insert("last_update_check_version", this->lastUpdateCheckVersion.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;
|
||||
}
|
||||
|
@ -631,6 +658,31 @@ 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::setLastUpdateCheckVersion(QVersionNumber version) {
|
||||
this->lastUpdateCheckVersion = version;
|
||||
this->save();
|
||||
}
|
||||
|
||||
void PorymapConfig::setRateLimitTimes(QMap<QUrl, QDateTime> map) {
|
||||
this->rateLimitTimes = map;
|
||||
this->save();
|
||||
}
|
||||
|
||||
QString PorymapConfig::getRecentProject() {
|
||||
return this->recentProjects.value(0);
|
||||
}
|
||||
|
@ -781,15 +833,26 @@ int PorymapConfig::getProjectSettingsTab() {
|
|||
return this->projectSettingsTab;
|
||||
}
|
||||
|
||||
void PorymapConfig::setWarpBehaviorWarningDisabled(bool disabled) {
|
||||
this->warpBehaviorWarningDisabled = disabled;
|
||||
this->save();
|
||||
}
|
||||
|
||||
bool PorymapConfig::getWarpBehaviorWarningDisabled() {
|
||||
return this->warpBehaviorWarningDisabled;
|
||||
}
|
||||
|
||||
bool PorymapConfig::getCheckForUpdates() {
|
||||
return this->checkForUpdates;
|
||||
}
|
||||
|
||||
QDateTime PorymapConfig::getLastUpdateCheckTime() {
|
||||
return this->lastUpdateCheckTime;
|
||||
}
|
||||
|
||||
QVersionNumber PorymapConfig::getLastUpdateCheckVersion() {
|
||||
return this->lastUpdateCheckVersion;
|
||||
}
|
||||
|
||||
QMap<QUrl, QDateTime> PorymapConfig::getRateLimitTimes() {
|
||||
return this->rateLimitTimes;
|
||||
}
|
||||
|
||||
const QStringList ProjectConfig::versionStrings = {
|
||||
"pokeruby",
|
||||
"pokefirered",
|
||||
|
|
154
src/core/network.cpp
Normal file
154
src/core/network.cpp
Normal file
|
@ -0,0 +1,154 @@
|
|||
#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;
|
||||
|
||||
// The url in the request and the url ultimately processed (reply->url()) may differ if the request was redirected.
|
||||
// For identification purposes (e.g. knowing if we are rate limited before a request is made) we use the url that
|
||||
// was originally given for the request.
|
||||
auto url = data->m_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.
|
||||
auto cacheEntry = this->cache.value(url, nullptr);
|
||||
if (cacheEntry)
|
||||
data->m_body = cacheEntry->data;
|
||||
else
|
||||
data->m_error = "Failed to read webpage from cache.";
|
||||
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.canConvert<QString>())
|
||||
cacheEntry->eTag = eTagHeader.toString();
|
||||
|
||||
cacheEntry->data = data->m_body = reply->readAll();
|
||||
}
|
|
@ -41,6 +41,12 @@
|
|||
#include <QSet>
|
||||
#include <QLoggingCategory>
|
||||
|
||||
// We only publish release binaries for Windows and macOS.
|
||||
// This is relevant for the update promoter, which alerts users of a new release.
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
||||
#define RELEASE_PLATFORM
|
||||
#endif
|
||||
|
||||
using OrderedJson = poryjson::Json;
|
||||
using OrderedJsonDoc = poryjson::JsonDoc;
|
||||
|
||||
|
@ -53,11 +59,13 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
{
|
||||
QCoreApplication::setOrganizationName("pret");
|
||||
QCoreApplication::setApplicationName("porymap");
|
||||
QCoreApplication::setApplicationVersion(PORYMAP_VERSION);
|
||||
QApplication::setApplicationDisplayName("porymap");
|
||||
QApplication::setWindowIcon(QIcon(":/icons/porymap-icon-2.ico"));
|
||||
ui->setupUi(this);
|
||||
|
||||
cleanupLargeLog();
|
||||
logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion()));
|
||||
|
||||
this->initWindow();
|
||||
if (porymapConfig.getReopenOnLaunch() && this->openProject(porymapConfig.getRecentProject(), true))
|
||||
|
@ -66,6 +74,9 @@ MainWindow::MainWindow(QWidget *parent) :
|
|||
// there is a bug affecting macOS users, where the trackpad deilveres a bad touch-release gesture
|
||||
// the warning is a bit annoying, so it is disabled here
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("qt.pointer.dispatch=false"));
|
||||
|
||||
if (porymapConfig.getCheckForUpdates())
|
||||
this->checkForUpdates(false);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
|
@ -91,6 +102,7 @@ void MainWindow::setWindowDisabled(bool disabled) {
|
|||
ui->actionAbout_Porymap->setDisabled(false);
|
||||
ui->actionOpen_Log_File->setDisabled(false);
|
||||
ui->actionOpen_Config_Folder->setDisabled(false);
|
||||
ui->actionCheck_for_Updates->setDisabled(false);
|
||||
if (!disabled)
|
||||
togglePreferenceSpecificUi();
|
||||
}
|
||||
|
@ -105,6 +117,10 @@ void MainWindow::initWindow() {
|
|||
this->initShortcuts();
|
||||
this->restoreWindowState();
|
||||
|
||||
#ifndef RELEASE_PLATFORM
|
||||
ui->actionCheck_for_Updates->setVisible(false);
|
||||
#endif
|
||||
|
||||
setWindowDisabled(true);
|
||||
}
|
||||
|
||||
|
@ -244,6 +260,39 @@ void MainWindow::initExtraSignals() {
|
|||
label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
}
|
||||
|
||||
void MainWindow::on_actionCheck_for_Updates_triggered() {
|
||||
checkForUpdates(true);
|
||||
}
|
||||
|
||||
#ifdef RELEASE_PLATFORM
|
||||
void MainWindow::checkForUpdates(bool requestedByUser) {
|
||||
if (!this->networkAccessManager)
|
||||
this->networkAccessManager = new NetworkAccessManager(this);
|
||||
|
||||
if (!this->updatePromoter) {
|
||||
this->updatePromoter = new UpdatePromoter(this, this->networkAccessManager);
|
||||
connect(this->updatePromoter, &UpdatePromoter::changedPreferences, [this] {
|
||||
if (this->preferenceEditor)
|
||||
this->preferenceEditor->updateFields();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
#else
|
||||
void MainWindow::checkForUpdates(bool) {}
|
||||
#endif
|
||||
|
||||
void MainWindow::initEditor() {
|
||||
this->editor = new Editor(ui);
|
||||
connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects);
|
||||
|
@ -2749,6 +2798,9 @@ void MainWindow::togglePreferenceSpecificUi() {
|
|||
ui->actionOpen_Project_in_Text_Editor->setEnabled(false);
|
||||
else
|
||||
ui->actionOpen_Project_in_Text_Editor->setEnabled(true);
|
||||
|
||||
if (this->updatePromoter)
|
||||
this->updatePromoter->updatePreferences();
|
||||
}
|
||||
|
||||
void MainWindow::openProjectSettingsEditor(int tab) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#include "scripting.h"
|
||||
#include "log.h"
|
||||
#include "config.h"
|
||||
#include "aboutporymap.h"
|
||||
|
||||
QMap<CallbackType, QString> callbackFunctions = {
|
||||
{OnProjectOpened, "onProjectOpened"},
|
||||
|
@ -77,15 +76,12 @@ void Scripting::populateGlobalObject(MainWindow *mainWindow) {
|
|||
|
||||
QJSValue constants = instance->engine->newObject();
|
||||
|
||||
// Invisibly create an "About" window to read Porymap version
|
||||
AboutPorymap *about = new AboutPorymap(mainWindow);
|
||||
if (about) {
|
||||
QJSValue version = Scripting::version(about->getVersionNumbers());
|
||||
constants.setProperty("version", version);
|
||||
delete about;
|
||||
} else {
|
||||
logError("Failed to read Porymap version for API");
|
||||
}
|
||||
// Get version numbers
|
||||
QJSValue version = instance->engine->newObject();
|
||||
version.setProperty("major", porymapVersion.majorVersion());
|
||||
version.setProperty("minor", porymapVersion.minorVersion());
|
||||
version.setProperty("patch", porymapVersion.microVersion());
|
||||
constants.setProperty("version", version);
|
||||
|
||||
// Get basic tileset information
|
||||
int numTilesPrimary = Project::getNumTilesPrimary();
|
||||
|
@ -343,14 +339,6 @@ QJSValue Scripting::position(int x, int y) {
|
|||
return obj;
|
||||
}
|
||||
|
||||
QJSValue Scripting::version(QList<int> versionNums) {
|
||||
QJSValue obj = instance->engine->newObject();
|
||||
obj.setProperty("major", versionNums.at(0));
|
||||
obj.setProperty("minor", versionNums.at(1));
|
||||
obj.setProperty("patch", versionNums.at(2));
|
||||
return obj;
|
||||
}
|
||||
|
||||
Tile Scripting::toTile(QJSValue obj) {
|
||||
Tile tile = Tile();
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ AboutPorymap::AboutPorymap(QWidget *parent) :
|
|||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
this->ui->label_Version->setText(QString("Version %1 - %2").arg(QCoreApplication::applicationVersion()).arg(QStringLiteral(__DATE__)));
|
||||
this->ui->textBrowser->setSource(QUrl("qrc:/CHANGELOG.md"));
|
||||
}
|
||||
|
||||
|
@ -15,16 +16,3 @@ AboutPorymap::~AboutPorymap()
|
|||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
// Returns the Porymap version number as a list of ints with the order {major, minor, patch}
|
||||
QList<int> AboutPorymap::getVersionNumbers()
|
||||
{
|
||||
// Get the version string "#.#.#"
|
||||
static const QRegularExpression regex("Version (\\d+)\\.(\\d+)\\.(\\d+)");
|
||||
QRegularExpressionMatch match = regex.match(ui->label_Version->text());
|
||||
if (!match.hasMatch()) {
|
||||
logError("Failed to locate Porymap version text");
|
||||
return QList<int>({0, 0, 0});
|
||||
}
|
||||
return QList<int>({match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt()});
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ void PreferenceEditor::updateFields() {
|
|||
ui->lineEdit_TextEditorGotoLine->setText(porymapConfig.getTextEditorGotoLine());
|
||||
ui->checkBox_MonitorProjectFiles->setChecked(porymapConfig.getMonitorFiles());
|
||||
ui->checkBox_OpenRecentProject->setChecked(porymapConfig.getReopenOnLaunch());
|
||||
ui->checkBox_CheckForUpdates->setChecked(porymapConfig.getCheckForUpdates());
|
||||
}
|
||||
|
||||
void PreferenceEditor::saveFields() {
|
||||
|
@ -58,10 +59,14 @@ void PreferenceEditor::saveFields() {
|
|||
emit themeChanged(theme);
|
||||
}
|
||||
|
||||
porymapConfig.setSaveDisabled(true);
|
||||
porymapConfig.setTextEditorOpenFolder(ui->lineEdit_TextEditorOpenFolder->text());
|
||||
porymapConfig.setTextEditorGotoLine(ui->lineEdit_TextEditorGotoLine->text());
|
||||
porymapConfig.setMonitorFiles(ui->checkBox_MonitorProjectFiles->isChecked());
|
||||
porymapConfig.setReopenOnLaunch(ui->checkBox_OpenRecentProject->isChecked());
|
||||
porymapConfig.setCheckForUpdates(ui->checkBox_CheckForUpdates->isChecked());
|
||||
porymapConfig.setSaveDisabled(false);
|
||||
porymapConfig.save();
|
||||
|
||||
emit preferencesSaved();
|
||||
}
|
||||
|
|
187
src/ui/updatepromoter.cpp
Normal file
187
src/ui/updatepromoter.cpp
Normal file
|
@ -0,0 +1,187 @@
|
|||
#include "updatepromoter.h"
|
||||
#include "ui_updatepromoter.h"
|
||||
#include "log.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QDesktopServices>
|
||||
#include <QTimer>
|
||||
|
||||
UpdatePromoter::UpdatePromoter(QWidget *parent, NetworkAccessManager *manager)
|
||||
: QDialog(parent),
|
||||
ui(new Ui::UpdatePromoter),
|
||||
manager(manager)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// 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);
|
||||
emit this->changedPreferences();
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
void UpdatePromoter::resetDialog() {
|
||||
this->button_Downloads->setEnabled(false);
|
||||
|
||||
ui->text_Changelog->setVisible(false);
|
||||
ui->label_Warning->setVisible(false);
|
||||
ui->label_Status->setText("");
|
||||
|
||||
this->changelog = QString();
|
||||
this->downloadUrl = QString();
|
||||
this->newVersion = QVersionNumber();
|
||||
this->foundReleases = false;
|
||||
this->visitedUrls.clear();
|
||||
}
|
||||
|
||||
void UpdatePromoter::checkForUpdates() {
|
||||
// If the Retry button is disabled, making requests is disabled
|
||||
if (!this->button_Retry->isEnabled())
|
||||
return;
|
||||
|
||||
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 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(), 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, const QUrl &nextUrl) {
|
||||
const QJsonArray releases = data.array();
|
||||
int i;
|
||||
for (i = 0; i < releases.size(); i++) {
|
||||
auto release = releases.at(i).toObject();
|
||||
|
||||
// Convert tag string to version numbers
|
||||
const QString tagName = release.value("tag_name").toString();
|
||||
const QVersionNumber version = QVersionNumber::fromString(tagName);
|
||||
if (version.segmentCount() != 3) 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.
|
||||
this->foundReleases = true;
|
||||
if (porymapVersion >= version)
|
||||
break;
|
||||
|
||||
const QString description = release.value("body").toString();
|
||||
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.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->downloadUrl.isEmpty()) {
|
||||
// This is the first (newest) release we've found. Record its URL for download.
|
||||
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->newVersion = version;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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 (!this->foundReleases) {
|
||||
// We retrieved the webpage but didn't successfully parse any releases.
|
||||
this->error("Error parsing releases webpage");
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate dialog with result
|
||||
ui->text_Changelog->setMarkdown(this->changelog);
|
||||
ui->text_Changelog->setVisible(!this->changelog.isEmpty());
|
||||
this->button_Downloads->setEnabled(!this->downloadUrl.isEmpty());
|
||||
this->button_Retry->setEnabled(true);
|
||||
if (!this->newVersion.isNull()) {
|
||||
ui->label_Status->setText("A new version of Porymap is available!");
|
||||
ui->label_Warning->setVisible(this->newVersion.majorVersion() > porymapVersion.majorVersion());
|
||||
|
||||
// Alert the user about the new version if the dialog wasn't already open.
|
||||
// Show the window, but also show the option to turn off automatic alerts in the future.
|
||||
// We only show this alert once for a given release.
|
||||
if (!this->isVisible() && this->newVersion > porymapConfig.getLastUpdateCheckVersion()) {
|
||||
ui->checkBox_StopAlerts->setVisible(true);
|
||||
this->show();
|
||||
}
|
||||
porymapConfig.setLastUpdateCheckVersion(this->newVersion);
|
||||
} else {
|
||||
ui->label_Status->setText("Your version of Porymap is up to date!");
|
||||
ui->label_Warning->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePromoter::error(const QString &err, const QDateTime retryAfter) {
|
||||
const QString message = QString("Failed to check for version update: %1").arg(err);
|
||||
ui->label_Status->setText(message);
|
||||
if (!this->isVisible())
|
||||
logWarn(message);
|
||||
|
||||
// If a "retry after" date/time is provided, disable the Retry button until then.
|
||||
// Otherwise users are allowed to retry after an error.
|
||||
auto timeUntil = QDateTime::currentDateTime().msecsTo(retryAfter);
|
||||
if (timeUntil > 0) {
|
||||
this->button_Retry->setEnabled(false);
|
||||
QTimer::singleShot(timeUntil, Qt::VeryCoarseTimer, [this]() {
|
||||
this->button_Retry->setEnabled(true);
|
||||
});
|
||||
} else {
|
||||
this->button_Retry->setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdatePromoter::updatePreferences() {
|
||||
const QSignalBlocker blocker(ui->checkBox_StopAlerts);
|
||||
ui->checkBox_StopAlerts->setChecked(!porymapConfig.getCheckForUpdates());
|
||||
}
|
||||
|
||||
void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) {
|
||||
if (ui->buttonBox->buttonRole(button) == QDialogButtonBox::RejectRole) {
|
||||
this->close();
|
||||
} else if (button == this->button_Retry) {
|
||||
this->checkForUpdates();
|
||||
} else if (button == this->button_Downloads) {
|
||||
QDesktopServices::openUrl(this->downloadUrl);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue