diff --git a/forms/updatepromoter.ui b/forms/updatepromoter.ui new file mode 100644 index 00000000..cbb4e8be --- /dev/null +++ b/forms/updatepromoter.ui @@ -0,0 +1,104 @@ + + + UpdatePromoter + + + + 0 + 0 + 592 + 484 + + + + Porymap Version Update + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + true + + + + + + + <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> + + + true + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + + + + + Do not alert me about new updates + + + + + + + + + + QDialogButtonBox::Close|QDialogButtonBox::Retry + + + + + + + + diff --git a/include/mainwindow.h b/include/mainwindow.h index 75c38fc9..c499a5ba 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -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 networkAccessManager = nullptr; private: QLabel *label_MapRulerStatus = nullptr; @@ -310,6 +310,8 @@ private: QPointer preferenceEditor = nullptr; QPointer projectSettingsEditor = nullptr; QPointer customScriptsEditor = nullptr; + QPointer updatePromoter = nullptr; + QPointer networkAccessManager = nullptr; FilterChildrenProxyModel *mapListProxyModel; QStandardItemModel *mapListModel; QList *mapGroupItemsList; diff --git a/include/ui/updatepromoter.h b/include/ui/updatepromoter.h new file mode 100644 index 00000000..4b0f209d --- /dev/null +++ b/include/ui/updatepromoter.h @@ -0,0 +1,44 @@ +#ifndef UPDATEPROMOTER_H +#define UPDATEPROMOTER_H + +#include +#include +#include +#include + +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 diff --git a/porymap.pro b/porymap.pro index f5cb0452..4e883b39 100644 --- a/porymap.pro +++ b/porymap.pro @@ -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 \ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f98b3a30..9fd45720 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -248,9 +248,6 @@ void MainWindow::initExtraSignals() { label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); } -// TODO: Relocate -#include -#include void MainWindow::on_actionCheck_for_Updates_triggered() { checkForUpdates(true); } @@ -259,63 +256,17 @@ 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 + if (!this->updatePromoter) { + this->updatePromoter = new UpdatePromoter(this, this->networkAccessManager); + connect(this->updatePromoter, &UpdatePromoter::changedPreferences, [this] { + if (this->preferenceEditor) + this->preferenceEditor->updateFields(); + }); } - 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 (requestedByUser) + this->updatePromoter->requestDialog(); + this->updatePromoter->checkForUpdates(); } void MainWindow::initEditor() { @@ -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) { diff --git a/src/ui/updatepromoter.cpp b/src/ui/updatepromoter.cpp new file mode 100644 index 00000000..c7dc9de7 --- /dev/null +++ b/src/ui/updatepromoter.cpp @@ -0,0 +1,177 @@ +#include "updatepromoter.h" +#include "ui_updatepromoter.h" +#include "log.h" +#include "config.h" + +#include +#include +#include +#include +#include + +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)); + } +}