Add update promoter dialog

This commit is contained in:
GriffinR 2024-01-21 02:00:28 -05:00
parent 97b485284e
commit c04a89396c
6 changed files with 346 additions and 62 deletions

104
forms/updatepromoter.ui Normal file
View 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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:13pt; color:#d7000c;&quot;&gt;WARNING: &lt;/span&gt;&lt;span style=&quot; font-weight:400;&quot;&gt;Updating Porymap may require you to update your projects. See &quot;Breaking Changes&quot; in the Changelog for details.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>

View file

@ -28,6 +28,7 @@
#include "preferenceeditor.h"
#include "projectsettingseditor.h"
#include "customscriptseditor.h"
#include "updatepromoter.h"
@ -298,7 +299,6 @@ private slots:
public:
Ui::MainWindow *ui;
Editor *editor = nullptr;
QPointer<QNetworkAccessManager> networkAccessManager = nullptr;
private:
QLabel *label_MapRulerStatus = nullptr;
@ -310,6 +310,8 @@ private:
QPointer<PreferenceEditor> preferenceEditor = nullptr;
QPointer<ProjectSettingsEditor> projectSettingsEditor = nullptr;
QPointer<CustomScriptsEditor> customScriptsEditor = nullptr;
QPointer<UpdatePromoter> updatePromoter = nullptr;
QPointer<QNetworkAccessManager> networkAccessManager = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
QStandardItemModel *mapListModel;
QList<QStandardItem*> *mapGroupItemsList;

View file

@ -0,0 +1,44 @@
#ifndef UPDATEPROMOTER_H
#define UPDATEPROMOTER_H
#include <QDialog>
#include <QPushButton>
#include <QNetworkAccessManager>
#include <QNetworkReply>
namespace Ui {
class UpdatePromoter;
}
class UpdatePromoter : public QDialog
{
Q_OBJECT
public:
explicit UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager);
~UpdatePromoter() {};
void checkForUpdates();
void requestDialog();
void updatePreferences();
private:
Ui::UpdatePromoter *ui;
QNetworkAccessManager *const manager;
QNetworkReply * reply = nullptr;
QPushButton * button_Downloads;
QString downloadLink;
QString changelog;
void resetDialog();
void processWebpage(const QJsonDocument &data);
void processError(const QString &err);
private slots:
void dialogButtonClicked(QAbstractButton *button);
signals:
void changedPreferences();
};
#endif // UPDATEPROMOTER_H

View file

@ -110,7 +110,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 \
@ -204,7 +205,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 \
@ -222,7 +224,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 \

View file

@ -248,9 +248,6 @@ void MainWindow::initExtraSignals() {
label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse);
}
// TODO: Relocate
#include <QNetworkReply>
#include <QNetworkRequest>
void MainWindow::on_actionCheck_for_Updates_triggered() {
checkForUpdates(true);
}
@ -259,65 +256,19 @@ void MainWindow::checkForUpdates(bool requestedByUser) {
if (!this->networkAccessManager)
this->networkAccessManager = new QNetworkAccessManager(this);
// We could get ".../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).
static const QUrl url("https://api.github.com/repos/huderlem/porymap/releases");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply * reply = this->networkAccessManager->get(request);
if (requestedByUser) {
// TODO: Show dialog
}
connect(reply, &QNetworkReply::finished, [this, reply] {
QJsonDocument data = QJsonDocument::fromJson(reply->readAll());
QJsonArray releases = data.array();
// Read all the items on the releases page, stopping when we find a tag that parses as a version identifier.
// Objects in the releases page data are sorted newest to oldest. Although I can't find a guarantee of this in
// GitHub's API documentation, this seems unlikely to change.
for (int i = 0; i < releases.size(); i++) {
auto release = releases.at(i).toObject();
const QStringList tag = release.value("tag_name").toString().split(".");
if (tag.length() != 3) continue;
bool ok;
int major = tag.at(0).toInt(&ok);
if (!ok) continue;
int minor = tag.at(1).toInt(&ok);
if (!ok) continue;
int patch = tag.at(2).toInt(&ok);
if (!ok) continue;
const QString downloadLink = release.value("html_url").toString();
if (downloadLink.isEmpty()) continue;
// We've found a valid release tag, we can stop reading.
logInfo(QString("Newest release is %1.%2.%3\n%4").arg(major).arg(minor).arg(patch).arg(downloadLink));
// If the release was published very recently it won't have a description yet, in which case don't tell the user.
const QString changelog = release.value("body").toString();
if (changelog.isEmpty()) break;
bool newVersionAvailable;
if (major != PORYMAP_VERSION_MAJOR) {
newVersionAvailable = major > PORYMAP_VERSION_MAJOR;
} else if (minor != PORYMAP_VERSION_MINOR) {
newVersionAvailable = minor > PORYMAP_VERSION_MINOR;
} else {
newVersionAvailable = patch > PORYMAP_VERSION_PATCH;
}
logInfo(QString("Host version is %1").arg(newVersionAvailable ? "old" : "up to date"));
// TODO: Show appropriate dialog
break;
}
if (!this->updatePromoter) {
this->updatePromoter = new UpdatePromoter(this, this->networkAccessManager);
connect(this->updatePromoter, &UpdatePromoter::changedPreferences, [this] {
if (this->preferenceEditor)
this->preferenceEditor->updateFields();
});
}
if (requestedByUser)
this->updatePromoter->requestDialog();
this->updatePromoter->checkForUpdates();
}
void MainWindow::initEditor() {
this->editor = new Editor(ui);
connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects);
@ -2812,6 +2763,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) {

177
src/ui/updatepromoter.cpp Normal file
View file

@ -0,0 +1,177 @@
#include "updatepromoter.h"
#include "ui_updatepromoter.h"
#include "log.h"
#include "config.h"
#include <QNetworkRequest>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDesktopServices>
UpdatePromoter::UpdatePromoter(QWidget *parent, QNetworkAccessManager *manager)
: QDialog(parent),
ui(new Ui::UpdatePromoter),
manager(manager)
{
ui->setupUi(this);
// Set up "Do not alert me" check box
this->updatePreferences();
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_Downloads = ui->buttonBox->addButton("Go to Downloads...", QDialogButtonBox::ActionRole);
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("Checking for updates...");
this->changelog = QString();
this->downloadLink = QString();
}
void UpdatePromoter::checkForUpdates() {
// Ignore request if one is still active.
if (this->reply && !this->reply->isFinished())
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
// 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());
}
});
}
// 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;
const QJsonArray releases = data.array();
for (int 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 QStringList tag = tagName.split(".");
if (tag.length() != 3) continue;
bool ok;
int major = tag.at(0).toInt(&ok);
if (!ok) continue;
int minor = tag.at(1).toInt(&ok);
if (!ok) continue;
int patch = tag.at(2).toInt(&ok);
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;
logInfo(QString("Found release %1.%2.%3").arg(major).arg(minor).arg(patch)); // TODO: Remove
if (major <= PORYMAP_VERSION_MAJOR && minor <= PORYMAP_VERSION_MINOR && patch <= PORYMAP_VERSION_PATCH)
break;
const QString description = release.value("body").toString();
const QString url = release.value("html_url").toString();
if (description.isEmpty() || url.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()) {
// This is the first (newest) release we've found. Record its URL for download.
this->downloadLink = url;
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");
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());
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!");
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);
}
}
// 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());
}
void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) {
auto buttonRole = ui->buttonBox->buttonRole(button);
if (buttonRole == QDialogButtonBox::RejectRole) {
this->close();
} else if (buttonRole == QDialogButtonBox::AcceptRole) {
// "Retry" button
this->checkForUpdates();
} else if (button == this->button_Downloads && !this->downloadLink.isEmpty()) {
QDesktopServices::openUrl(QUrl(this->downloadLink));
}
}