mirror of
https://github.com/Mudlet/Mudlet
synced 2026-08-13 18:26:27 -04:00
Fix: updater to work with github releases (#9125)
<!-- Keep the title short & concise so anyone non-technical can
understand it,
the title appears in PTB changelogs -->
#### Brief overview of PR changes/additions
Migrate updater off dblsqd to work with github releases instead for both
official releases and PTBs.
#### Motivation for adding to Mudlet
dblsqd infrastructure doesn't work anymore.
#### Other info (issues closed, discussion etc)
The CI part of this where github releases are setup in the right format
is not yet ready.
---------
Co-authored-by: Stephen Lyons <slysven@virginmedia.com>
Co-authored-by: Vadim Peretokin <vadi2@users.noreply.github.com>
This commit is contained in:
parent
bc9ac84841
commit
d2ab992c47
19 changed files with 2474 additions and 241 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
|
@ -4,9 +4,6 @@
|
|||
[submodule "3rdparty/lua_code_formatter"]
|
||||
path = 3rdparty/lcf
|
||||
url = https://github.com/martin-eden/lua_code_formatter.git
|
||||
[submodule "3rdparty/dblsqd"]
|
||||
path = 3rdparty/dblsqd
|
||||
url = https://github.com/Mudlet/dblsqd-sdk-qt.git
|
||||
[submodule "3rdparty/qt-tags-widget"]
|
||||
path = 3rdparty/qt-tags-widget
|
||||
url = https://github.com/julian-go/qt-tags-widget.git
|
||||
|
|
|
|||
1
3rdparty/dblsqd
vendored
1
3rdparty/dblsqd
vendored
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 692697328a8312c951df12f07f8c8068d8ae24e7
|
||||
|
|
@ -192,7 +192,7 @@ find_package(Qt6 COMPONENTS Core5Compat REQUIRED)
|
|||
|
||||
find_package(Lua 5.1 EXACT)
|
||||
|
||||
# Set Qt version for edbee-lib and dblsqd
|
||||
# Set Qt version for edbee-lib
|
||||
option(BUILD_WITH_QT5 "" OFF)
|
||||
option(BUILD_WITH_QT6 "" ON)
|
||||
|
||||
|
|
@ -232,10 +232,6 @@ git_submodule_init(
|
|||
# GIT_SHALLOW TRUE)
|
||||
# FetchContent_MakeAvailable(nanobench)
|
||||
|
||||
if(USE_UPDATER)
|
||||
git_submodule_init(CHECK_FILE "3rdparty/dblsqd/CMakeLists.txt" SUBMODULE_PATH
|
||||
"3rdparty/dblsqd" READABLE_NAME "DBLSQD updater")
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
if(USE_UPDATER)
|
||||
|
|
@ -267,9 +263,6 @@ add_subdirectory(translations/translated)
|
|||
add_subdirectory(src)
|
||||
add_subdirectory(test)
|
||||
|
||||
if(USE_UPDATER)
|
||||
add_subdirectory(3rdparty/dblsqd)
|
||||
endif()
|
||||
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE -O3)
|
||||
|
|
|
|||
|
|
@ -436,6 +436,15 @@ set(mudlet_HDRS
|
|||
if(USE_UPDATER)
|
||||
list(APPEND mudlet_SRCS updater.cpp)
|
||||
list(APPEND mudlet_HDRS updater.h)
|
||||
list(APPEND mudlet_SRCS updater/Feed.cpp)
|
||||
list(APPEND mudlet_HDRS updater/Feed.h)
|
||||
list(APPEND mudlet_SRCS updater/Release.cpp)
|
||||
list(APPEND mudlet_HDRS updater/Release.h)
|
||||
list(APPEND mudlet_SRCS updater/SemVer.cpp)
|
||||
list(APPEND mudlet_HDRS updater/SemVer.h)
|
||||
list(APPEND mudlet_SRCS updater/UpdateDialog.cpp)
|
||||
list(APPEND mudlet_HDRS updater/UpdateDialog.h)
|
||||
list(APPEND mudlet_UIS updater/update_dialog.ui)
|
||||
endif(USE_UPDATER)
|
||||
|
||||
if(USE_3DMAPPER)
|
||||
|
|
@ -707,8 +716,8 @@ if (SYSINFO_INCLUDE_DIR)
|
|||
endif (SYSINFO_INCLUDE_DIR)
|
||||
|
||||
if(USE_UPDATER)
|
||||
target_link_libraries(${LIB_MUDLET_TARGET} dblsqd)
|
||||
target_compile_definitions(${LIB_MUDLET_TARGET} PUBLIC INCLUDE_UPDATER)
|
||||
target_include_directories(${LIB_MUDLET_TARGET} PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
|
||||
endif(USE_UPDATER)
|
||||
|
||||
if(USE_3DMAPPER)
|
||||
|
|
@ -804,6 +813,24 @@ if(APPLE)
|
|||
file(GLOB ICON_FILE "icons/mudlet.icns")
|
||||
endif()
|
||||
|
||||
# Set Sparkle feed URL in Info.plist for PTB and release builds
|
||||
if(USE_UPDATER)
|
||||
if(APP_BUILD MATCHES "-ptb.+")
|
||||
set(SPARKLE_CHANNEL "ptb")
|
||||
elseif(NOT (APP_BUILD MATCHES "-dev.+" OR APP_BUILD MATCHES "-test.+"))
|
||||
set(SPARKLE_CHANNEL "release")
|
||||
endif()
|
||||
|
||||
if(SPARKLE_CHANNEL)
|
||||
set(SPARKLE_FEED_URL "https://www.mudlet.org/wp-content/files/appcast/${SPARKLE_CHANNEL}-${CMAKE_SYSTEM_PROCESSOR}.xml")
|
||||
add_custom_command(TARGET ${EXE_MUDLET_TARGET} POST_BUILD
|
||||
COMMAND bash -c
|
||||
"/usr/libexec/PlistBuddy -c 'Delete :SUFeedURL' '$<TARGET_BUNDLE_DIR:${EXE_MUDLET_TARGET}>/Contents/Info.plist' 2>/dev/null || true && /usr/libexec/PlistBuddy -c 'Add :SUFeedURL string ${SPARKLE_FEED_URL}' '$<TARGET_BUNDLE_DIR:${EXE_MUDLET_TARGET}>/Contents/Info.plist'"
|
||||
COMMENT "Setting Sparkle feed URL for ${SPARKLE_CHANNEL}-${CMAKE_SYSTEM_PROCESSOR}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE MUDLET_LUA_FILES LIST_DIRECTORIES true "mudlet-lua/*")
|
||||
file(GLOB_RECURSE LUA_TRANSLATIONS LIST_DIRECTORIES true "../translations/lua/*")
|
||||
file(GLOB DIC_FILES "*.dic")
|
||||
|
|
|
|||
|
|
@ -885,7 +885,7 @@ void dlgAboutDialog::setThirdPartyTab(const QString& htmlHead) const
|
|||
"All rights reserved.</h3>"));
|
||||
|
||||
#if defined(INCLUDE_UPDATER) || defined(DEBUG_SHOWALL)
|
||||
QString DblsqdHeader(tr("<h2><u>Dblsqd</u></h2>"
|
||||
QString DblsqdHeader(tr("<h2><u>Dblsqd (derived work)</u></h2>"
|
||||
"<h3>Copyright © 2017 Philipp Medien</h3>"));
|
||||
#if defined(Q_OS_MACOS) || defined(DEBUG_SHOWALL)
|
||||
QString SparkleHeader(tr("<h2><u>Sparkle - macOS updater</u></h2>"
|
||||
|
|
|
|||
|
|
@ -740,7 +740,7 @@ void mudlet::init()
|
|||
connect(this, &mudlet::signal_windowStateChanged, this, &mudlet::slot_windowStateChanged);
|
||||
|
||||
#if defined(INCLUDE_UPDATER)
|
||||
pUpdater = new Updater(this, mpSettings, publicTestVersion);
|
||||
pUpdater = new Updater(this, mpSettings, !releaseVersion);
|
||||
connect(pUpdater, &Updater::signal_updateAvailable, this, &mudlet::slot_updateAvailable);
|
||||
connect(pUpdater, &Updater::signal_updateCheckFailed, this, &mudlet::slot_updateCheckFailed);
|
||||
connect(dactionUpdate, &QAction::triggered, this, &mudlet::slot_manualUpdateCheck);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class SparkleUpdater : public QObject
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SparkleUpdater();
|
||||
explicit SparkleUpdater(QObject* parent = nullptr);
|
||||
~SparkleUpdater();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@
|
|||
|
||||
@end
|
||||
|
||||
SparkleUpdater::SparkleUpdater()
|
||||
SparkleUpdater::SparkleUpdater(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@autoreleasepool {
|
||||
_updaterDelegate = [[SparkleUpdaterDelegate alloc] init];
|
||||
|
|
|
|||
450
src/updater.cpp
450
src/updater.cpp
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
#include "updater.h"
|
||||
#include "mudlet.h"
|
||||
#include "updater/Feed.h"
|
||||
#include "updater/UpdateDialog.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QMessageBox>
|
||||
|
|
@ -27,15 +29,10 @@
|
|||
#include <chrono>
|
||||
#include "../3rdparty/kdtoolbox/singleshot_connect/singleshot_connect.h"
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
// Helper function to clean up .nupkg files from SquirrelTemp directory
|
||||
// This prevents cross-contamination with other Squirrel-based apps
|
||||
// Clean up legacy .nupkg files from the previous Squirrel/dblsqd update system's temp directory
|
||||
static void cleanupSquirrelTempFiles()
|
||||
{
|
||||
QString squirrelTempPath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + qsl("/SquirrelTemp");
|
||||
|
|
@ -47,7 +44,6 @@ static void cleanupSquirrelTempFiles()
|
|||
|
||||
qDebug() << "Cleaning up Mudlet files from SquirrelTemp:" << squirrelTempPath;
|
||||
|
||||
// Find all Mudlet-related .nupkg files
|
||||
QStringList filters;
|
||||
filters << qsl("Mudlet*.nupkg") << qsl("mudlet*.nupkg");
|
||||
QFileInfoList nupkgFiles = squirrelTempDir.entryInfoList(filters, QDir::Files);
|
||||
|
|
@ -67,53 +63,39 @@ static void cleanupSquirrelTempFiles()
|
|||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
qWarning() << "Cleaned up" << removedCount << "Mudlet .nupkg files from SquirrelTemp, freed" << (freedSpace / 1024 / 1024) << "MB of disk space";
|
||||
qDebug() << "Cleaned up" << removedCount << "Mudlet .nupkg files from SquirrelTemp, freed" << (freedSpace / 1024 / 1024) << "MB of disk space";
|
||||
}
|
||||
}
|
||||
#endif // Q_OS_WINDOWS
|
||||
|
||||
// update flows:
|
||||
// linux: new AppImage is downloaded, unzipped, and put in place of the old one
|
||||
// linux: new AppImage is downloaded, extracted from its tar archive, and put in place of the old one
|
||||
// user then only restarts mudlet to get the new version
|
||||
// windows: new squirrel installer is downloaded and saved
|
||||
// user then restarts, mudlet sees that there's a new installer available: launches it
|
||||
// and promptly quits. Installer updates Mudlet and launches Mudlet when its done
|
||||
// windows: installer .exe is downloaded from GitHub Releases. When the user clicks restart,
|
||||
// a batch file is created that waits for Mudlet to exit, then runs the installer
|
||||
// mac: handled completely outside of Mudlet by Sparkle
|
||||
|
||||
Updater::Updater(QObject* parent, QSettings* settings, bool testVersion)
|
||||
: QObject(parent)
|
||||
#if !defined(Q_OS_MACOS)
|
||||
//: Label for the update/restart button in the main toolbar
|
||||
, mpInstallOrRestart(new QPushButton(tr("Update")))
|
||||
#endif
|
||||
, mUpdateInstalled(false)
|
||||
{
|
||||
Q_ASSERT_X(settings, "updater", "QSettings object is required for the updater to work");
|
||||
this->settings = settings;
|
||||
mSettings = settings;
|
||||
|
||||
QString baseUrl = QStringLiteral("https://feeds.dblsqd.com/MKMMR7HNSP65PquQQbiDIw");
|
||||
QString channel = testVersion ? QStringLiteral("public-test-build") : QStringLiteral("release");
|
||||
|
||||
// On 32-bit Windows, check if we can upgrade to 64-bit
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
QString arch = is64BitCompatible() ? QStringLiteral("x86_64") : QStringLiteral("x86");
|
||||
#else
|
||||
QString arch = QString(); // Let Feed auto-detect for other platforms
|
||||
#endif
|
||||
|
||||
feed = new dblsqd::Feed();
|
||||
feed->setUrl(baseUrl, channel, QString(), arch, QString());
|
||||
|
||||
if (!mDailyCheck) {
|
||||
mDailyCheck = std::make_unique<QTimer>();
|
||||
}
|
||||
mFeed = new dblsqd::Feed(this);
|
||||
mFeed->setRepo(qsl("Mudlet"), qsl("Mudlet"), testVersion);
|
||||
mPeriodicCheck = std::make_unique<QTimer>();
|
||||
}
|
||||
|
||||
Updater::~Updater()
|
||||
{
|
||||
delete (feed);
|
||||
delete mUpdateDialog;
|
||||
}
|
||||
|
||||
// start the update process and figure out what needs to be done.
|
||||
// If it's a silent update, do that right away, otherwise
|
||||
// setup manual updates to do our custom actions
|
||||
void Updater::checkUpdatesOnStart()
|
||||
{
|
||||
#if defined(Q_OS_MACOS)
|
||||
|
|
@ -124,11 +106,11 @@ void Updater::checkUpdatesOnStart()
|
|||
setupOnWindows();
|
||||
#endif
|
||||
|
||||
mDailyCheck->setInterval(12h);
|
||||
connect(mDailyCheck.get(), &QTimer::timeout, this, [this] {
|
||||
KDToolBox::connectSingleShot(feed, &dblsqd::Feed::ready, this, [this]() {
|
||||
auto updates = feed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
qWarning() << "Bi-daily check for updates:" << updates.size() << "update(s) available";
|
||||
mPeriodicCheck->setInterval(12h);
|
||||
connect(mPeriodicCheck.get(), &QTimer::timeout, this, [this] {
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::ready, this, [this]() {
|
||||
auto updates = mFeed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
qWarning() << "Twice-daily check for updates:" << updates.size() << "update(s) available";
|
||||
if (updates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -138,14 +120,16 @@ void Updater::checkUpdatesOnStart()
|
|||
return;
|
||||
}
|
||||
|
||||
downloadReleaseIfValid(updates.first());
|
||||
if (!downloadReleaseIfValid(updates.first())) {
|
||||
emit signal_updateAvailable(updates.size());
|
||||
}
|
||||
});
|
||||
KDToolBox::connectSingleShot(feed, &dblsqd::Feed::loadError, this, [](const QString& error) {
|
||||
qWarning() << "Bi-daily update check: failed to load feed:" << error;
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::loadError, this, [](const QString& error) {
|
||||
qWarning() << "Twice-daily update check: failed to load feed:" << error;
|
||||
});
|
||||
feed->load();
|
||||
mFeed->load();
|
||||
});
|
||||
mDailyCheck->start();
|
||||
mPeriodicCheck->start();
|
||||
}
|
||||
|
||||
void Updater::setAutomaticUpdates(const bool state)
|
||||
|
|
@ -153,7 +137,7 @@ void Updater::setAutomaticUpdates(const bool state)
|
|||
#if defined(Q_OS_MACOS)
|
||||
msparkleUpdater->setAutomaticallyDownloadsUpdates(state);
|
||||
#else
|
||||
dblsqd::UpdateDialog::enableAutoDownload(state, settings);
|
||||
dblsqd::UpdateDialog::enableAutoDownload(state, mSettings);
|
||||
#endif
|
||||
// The sense of this control is inverted on the dlgProfilePreferences - so
|
||||
// must be inverted here:
|
||||
|
|
@ -165,7 +149,7 @@ bool Updater::updateAutomatically() const
|
|||
#if defined(Q_OS_MACOS)
|
||||
return msparkleUpdater->automaticallyDownloadsUpdates();
|
||||
#else
|
||||
return dblsqd::UpdateDialog::autoDownloadEnabled(true, settings);
|
||||
return dblsqd::UpdateDialog::autoDownloadEnabled(true, mSettings);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -179,12 +163,12 @@ void Updater::manuallyCheckUpdates()
|
|||
}
|
||||
mManualCheckInProgress = true;
|
||||
|
||||
feed->load();
|
||||
KDToolBox::connectSingleShot(feed, &dblsqd::Feed::ready, this, [this]() {
|
||||
mFeed->load();
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::ready, this, [this]() {
|
||||
mManualCheckInProgress = false;
|
||||
showDialogManually();
|
||||
});
|
||||
KDToolBox::connectSingleShot(feed, &dblsqd::Feed::loadError, this, [this](const QString& error) {
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::loadError, this, [this](const QString& error) {
|
||||
mManualCheckInProgress = false;
|
||||
emit signal_updateCheckFailed(error);
|
||||
});
|
||||
|
|
@ -193,34 +177,45 @@ void Updater::manuallyCheckUpdates()
|
|||
|
||||
void Updater::showDialogManually() const
|
||||
{
|
||||
updateDialog->show();
|
||||
if (!mUpdateDialog) {
|
||||
qWarning() << "showDialogManually called but update dialog not initialized";
|
||||
return;
|
||||
}
|
||||
mUpdateDialog->show();
|
||||
}
|
||||
|
||||
// only shows the changelog since the last version
|
||||
void Updater::showChangelog() const
|
||||
{
|
||||
auto changelogDialog = new dblsqd::UpdateDialog(feed, dblsqd::UpdateDialog::ManualChangelog);
|
||||
auto changelogDialog = new dblsqd::UpdateDialog(mFeed, dblsqd::UpdateDialog::ManualChangelog, mSettings);
|
||||
changelogDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
changelogDialog->setPreviousVersion(getPreviousVersion());
|
||||
changelogDialog->show();
|
||||
}
|
||||
|
||||
// shows the full changelog
|
||||
void Updater::showFullChangelog() const
|
||||
{
|
||||
if (!feed->isReady()) {
|
||||
KDToolBox::connectSingleShot(feed, &dblsqd::Feed::ready, feed, [=, this]() {
|
||||
showChangelog();
|
||||
if (!mFeed->isReady()) {
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::ready, mFeed, [=, this]() {
|
||||
showFullChangelog();
|
||||
});
|
||||
feed->load();
|
||||
KDToolBox::connectSingleShot(mFeed, &dblsqd::Feed::loadError, mFeed, [](const QString& error) {
|
||||
qWarning() << "Failed to load feed for changelog:" << error;
|
||||
//: Error title for dialog shown when changelog fails to load
|
||||
QMessageBox::warning(nullptr,
|
||||
tr("Changelog Error"),
|
||||
//: Error message shown when changelog fails to load from the server
|
||||
tr("Could not load the changelog. Please try again later."));
|
||||
});
|
||||
mFeed->load();
|
||||
return;
|
||||
}
|
||||
|
||||
auto changelogDialog = new dblsqd::UpdateDialog(feed, dblsqd::UpdateDialog::ManualChangelog);
|
||||
auto changelogDialog = new dblsqd::UpdateDialog(mFeed, dblsqd::UpdateDialog::ManualChangelog, mSettings);
|
||||
changelogDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
auto releases = feed->getReleases();
|
||||
const auto firstVersion = releases.constLast().getVersion();
|
||||
changelogDialog->setMinVersion(firstVersion);
|
||||
auto releases = mFeed->getReleases();
|
||||
if (!releases.isEmpty()) {
|
||||
changelogDialog->setMinVersion(releases.constLast().getVersion());
|
||||
}
|
||||
changelogDialog->setMaxVersion(QApplication::applicationVersion());
|
||||
changelogDialog->show();
|
||||
}
|
||||
|
|
@ -231,21 +226,30 @@ bool Updater::downloadReleaseIfValid(const dblsqd::Release& release)
|
|||
if (!downloadUrl.isValid() || downloadUrl.isEmpty()) {
|
||||
qWarning() << "Update check: invalid download URL for release" << release.getVersion();
|
||||
if (mManualCheckInProgress) {
|
||||
emit signal_updateCheckFailed(tr("Invalid download URL for version %1").arg(release.getVersion()));
|
||||
//: Error shown when no download is available for the user's platform. %1 is the version number.
|
||||
emit signal_updateCheckFailed(tr("No download available for version %1. Please try again later or download manually from https://www.mudlet.org/download/").arg(release.getVersion()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
feed->downloadRelease(release);
|
||||
mFeed->downloadRelease(release);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Updater::finishSetup()
|
||||
{
|
||||
auto updates = mFeed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
#if defined(Q_OS_LINUX)
|
||||
qWarning() << "Successfully updated Mudlet to" << feed->getUpdates(dblsqd::Release::getCurrentRelease()).constFirst().getVersion();
|
||||
if (!updates.isEmpty()) {
|
||||
qWarning() << "Successfully updated Mudlet to" << updates.constFirst().getVersion();
|
||||
} else {
|
||||
qWarning() << "Update finished but could not determine target version";
|
||||
}
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
qWarning() << "Mudlet prepped to update to" << feed->getUpdates(dblsqd::Release::getCurrentRelease()).first().getVersion() << "on restart";
|
||||
// Clean up .nupkg files from SquirrelTemp to prevent cross-app contamination
|
||||
if (!updates.isEmpty()) {
|
||||
qWarning() << "Mudlet prepped to update to" << updates.first().getVersion() << "on restart";
|
||||
} else {
|
||||
qWarning() << "Mudlet prepped to update on restart";
|
||||
}
|
||||
cleanupSquirrelTempFiles();
|
||||
#endif
|
||||
recordUpdateTime();
|
||||
|
|
@ -258,134 +262,108 @@ void Updater::finishSetup()
|
|||
void Updater::setupOnMacOS()
|
||||
{
|
||||
// don't need to explicitly check for updates - sparkle will do so on its own
|
||||
msparkleUpdater = new SparkleUpdater();
|
||||
msparkleUpdater = new SparkleUpdater(this);
|
||||
}
|
||||
#endif // Q_OS_MACOS
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
void Updater::setupOnWindows()
|
||||
#if !defined(Q_OS_MACOS)
|
||||
void Updater::setupPlatformUpdater()
|
||||
{
|
||||
// Clean up old .nupkg files on startup
|
||||
cleanupSquirrelTempFiles();
|
||||
|
||||
// Setup to automatically download the new release when an update is available
|
||||
connect(feed, &dblsqd::Feed::ready, feed, [=, this]() {
|
||||
connect(mFeed, &dblsqd::Feed::ready, this, [=, this]() {
|
||||
auto* pMudlet = mudlet::self();
|
||||
if (!pMudlet || pMudlet->developmentVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto updates = feed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
auto updates = mFeed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
qWarning() << "Checked for updates:" << updates.size() << "update(s) available";
|
||||
if (updates.isEmpty()) {
|
||||
return;
|
||||
} else if (!updateAutomatically()) {
|
||||
if (!updates.isEmpty()) {
|
||||
emit signal_updateAvailable(updates.size());
|
||||
} else {
|
||||
downloadReleaseIfValid(updates.first());
|
||||
}
|
||||
});
|
||||
|
||||
// Setup to run setup.exe to replace the old installation
|
||||
connect(feed, &dblsqd::Feed::downloadFinished, this, [=, this]() {
|
||||
// if automatic updates are enabled, and this isn't a manual check, perform the automatic update
|
||||
if (!(updateAutomatically() && updateDialog->isHidden())) {
|
||||
connect(mFeed, &dblsqd::Feed::downloadError, this, [this](const QString& error) {
|
||||
qWarning() << "Automatic update download failed:" << error;
|
||||
emit signal_updateCheckFailed(error);
|
||||
});
|
||||
|
||||
mUpdateDialog = new dblsqd::UpdateDialog(mFeed, updateAutomatically() ? dblsqd::UpdateDialog::OnLastWindowClosed : dblsqd::UpdateDialog::Manual, mSettings);
|
||||
//: Label for the update button shown in the update dialog
|
||||
mpInstallOrRestart->setText(tr("Update"));
|
||||
mUpdateDialog->addInstallButton(mpInstallOrRestart);
|
||||
connect(mUpdateDialog, &dblsqd::UpdateDialog::installButtonClicked, this, &Updater::slot_installOrRestartClicked);
|
||||
}
|
||||
#endif // !Q_OS_MACOS
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
void Updater::setupOnWindows()
|
||||
{
|
||||
cleanupSquirrelTempFiles();
|
||||
setupPlatformUpdater();
|
||||
|
||||
connect(mFeed, &dblsqd::Feed::downloadFinished, this, [=, this]() {
|
||||
if (!(updateAutomatically() && mUpdateDialog->isHidden())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* downloadFile = feed->getDownloadFile();
|
||||
if (!downloadFile) {
|
||||
qWarning() << "Download finished but no download file available - feed URL:" << feed->getUrl();
|
||||
const QString fileName = mFeed->getDownloadFilePath();
|
||||
if (fileName.isEmpty()) {
|
||||
qWarning() << "Download finished but no download file available - feed URL:" << mFeed->getUrl();
|
||||
//: Error shown when the automatic update download finished but produced no file
|
||||
emit signal_updateCheckFailed(tr("Update download failed. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
const QString fileName = downloadFile->fileName();
|
||||
|
||||
QFuture<void> future = QtConcurrent::run([=, this]() {
|
||||
prepareSetupOnWindows(fileName);
|
||||
});
|
||||
|
||||
// replace current binary with the unzipped one
|
||||
auto watcher = new QFutureWatcher<void>;
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, &Updater::finishSetup);
|
||||
connect(watcher, &QFutureWatcher<void>::finished, watcher, &QObject::deleteLater);
|
||||
watcher->setFuture(future);
|
||||
});
|
||||
|
||||
// finally, create the dblsqd objects. Constructing the UpdateDialog triggers the update check
|
||||
updateDialog = new dblsqd::UpdateDialog(feed, updateAutomatically() ? dblsqd::UpdateDialog::OnLastWindowClosed : dblsqd::UpdateDialog::Manual, nullptr, settings);
|
||||
mpInstallOrRestart->setText(tr("Update"));
|
||||
updateDialog->addInstallButton(mpInstallOrRestart);
|
||||
connect(updateDialog, &dblsqd::UpdateDialog::installButtonClicked, this, &Updater::slot_installOrRestartClicked);
|
||||
}
|
||||
|
||||
// Store the path to the downloaded installer for use when user clicks "Restart to update"
|
||||
void Updater::prepareSetupOnWindows(const QString& downloadedSetupName)
|
||||
{
|
||||
mDownloadedInstallerPath = downloadedSetupName;
|
||||
qWarning() << "Installer ready at:" << mDownloadedInstallerPath;
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
#endif // Q_OS_WINDOWS
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
void Updater::setupOnLinux()
|
||||
{
|
||||
// Setup to automatically download the new release when an update is
|
||||
// available or wave a flag when it is to be done manually
|
||||
// Setup to automatically download the new release when an update is available
|
||||
connect(feed, &dblsqd::Feed::ready, this, [=, this]() {
|
||||
// don't update development builds to prevent auto-update from overwriting your
|
||||
// compiled binary while in development
|
||||
auto* pMudlet = mudlet::self();
|
||||
if (!pMudlet || pMudlet->developmentVersion) {
|
||||
setupPlatformUpdater();
|
||||
|
||||
connect(mFeed, &dblsqd::Feed::downloadFinished, this, [=, this]() {
|
||||
if (!(updateAutomatically() && mUpdateDialog->isHidden())) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto updates = feed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
qWarning() << "Checked for updates:" << updates.size() << "update(s) available";
|
||||
if (updates.isEmpty()) {
|
||||
return;
|
||||
} else if (!updateAutomatically()) {
|
||||
emit signal_updateAvailable(updates.size());
|
||||
return;
|
||||
} else {
|
||||
downloadReleaseIfValid(updates.first());
|
||||
}
|
||||
});
|
||||
|
||||
// Setup to unzip and replace old binary when the download is done
|
||||
connect(feed, &dblsqd::Feed::downloadFinished, this, [=, this]() {
|
||||
// if automatic updates are enabled, and this isn't a manual check, perform the automatic update
|
||||
if (!(updateAutomatically() && updateDialog->isHidden())) {
|
||||
const QString fileName = mFeed->getDownloadFilePath();
|
||||
if (fileName.isEmpty()) {
|
||||
qWarning() << "Download finished but no download file available - feed URL:" << mFeed->getUrl();
|
||||
//: Error shown when the automatic update download finished but produced no file
|
||||
emit signal_updateCheckFailed(tr("Update download failed. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
|
||||
auto* downloadFile = feed->getDownloadFile();
|
||||
if (!downloadFile) {
|
||||
qWarning() << "Download finished but no download file available - feed URL:" << feed->getUrl();
|
||||
return;
|
||||
}
|
||||
const QString fileName = downloadFile->fileName();
|
||||
|
||||
QFuture<void> future = QtConcurrent::run([=, this]() {
|
||||
untarOnLinux(fileName);
|
||||
});
|
||||
|
||||
// replace current binary with the unzipped one
|
||||
auto watcher = new QFutureWatcher<void>;
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, &Updater::slot_updateLinuxBinary);
|
||||
connect(watcher, &QFutureWatcher<void>::finished, watcher, &QObject::deleteLater);
|
||||
watcher->setFuture(future);
|
||||
});
|
||||
|
||||
// finally, create the dblsqd objects. Constructing the UpdateDialog triggers the update check
|
||||
updateDialog = new dblsqd::UpdateDialog(feed, updateAutomatically() ? dblsqd::UpdateDialog::OnLastWindowClosed : dblsqd::UpdateDialog::Manual, nullptr, settings);
|
||||
mpInstallOrRestart->setText(tr("Update"));
|
||||
updateDialog->addInstallButton(mpInstallOrRestart);
|
||||
connect(updateDialog, &dblsqd::UpdateDialog::installButtonClicked, this, &Updater::slot_installOrRestartClicked);
|
||||
}
|
||||
|
||||
void Updater::untarOnLinux(const QString& fileName)
|
||||
{
|
||||
mUnzippedBinaryName.clear();
|
||||
Q_ASSERT_X(QThread::currentThread() != QCoreApplication::instance()->thread(), "untarOnLinux", "method should not be called in the main GUI thread to avoid a degradation in UX");
|
||||
qWarning() << __func__ << "started";
|
||||
|
||||
|
|
@ -394,10 +372,20 @@ void Updater::untarOnLinux(const QString& fileName)
|
|||
// we can assume tar to be present on a Linux system. If it's not, it'd be rather broken.
|
||||
// tar output folder has to end with a slash
|
||||
tar.start(qsl("tar"), QStringList() << qsl("-xvf") << fileName << qsl("-C") << QStandardPaths::writableLocation(QStandardPaths::TempLocation) + qsl("/"));
|
||||
if (!tar.waitForFinished()) {
|
||||
qWarning() << "Untarring" << fileName << "failed:" << tar.errorString();
|
||||
if (!tar.waitForStarted(5000)) {
|
||||
qWarning() << "Could not start tar:" << tar.errorString();
|
||||
} else if (!tar.waitForFinished(300000)) {
|
||||
tar.kill();
|
||||
qWarning() << "Untarring" << fileName << "timed out after 5 minutes:" << tar.errorString();
|
||||
} else if (tar.exitCode() != 0) {
|
||||
qWarning() << "Untarring" << fileName << "failed - exit code:" << tar.exitCode() << tar.errorString();
|
||||
} else {
|
||||
unzippedBinaryName = tar.readAll().trimmed();
|
||||
const QString output = tar.readAll().trimmed();
|
||||
if (output.isEmpty() || output.contains(QLatin1Char('\n'))) {
|
||||
qWarning() << "Unexpected tar output (expected single filename):" << output;
|
||||
} else {
|
||||
mUnzippedBinaryName = output;
|
||||
}
|
||||
}
|
||||
qWarning() << __func__ << "finished";
|
||||
}
|
||||
|
|
@ -406,7 +394,14 @@ void Updater::slot_updateLinuxBinary()
|
|||
{
|
||||
qWarning() << __func__ << "started";
|
||||
|
||||
QFileInfo unzippedBinary(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + unzippedBinaryName);
|
||||
if (mUnzippedBinaryName.isEmpty()) {
|
||||
qWarning() << "Extraction failed - no binary to install, aborting update";
|
||||
//: Error shown when extracting the downloaded update archive fails on Linux
|
||||
emit signal_updateCheckFailed(tr("Failed to extract the update. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
|
||||
QFileInfo unzippedBinary(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + mUnzippedBinaryName);
|
||||
auto systemEnvironment = QProcessEnvironment::systemEnvironment();
|
||||
auto appimageLocation = systemEnvironment.contains(qsl("APPIMAGE")) ? systemEnvironment.value(qsl("APPIMAGE"), QString()) : QCoreApplication::applicationFilePath();
|
||||
|
||||
|
|
@ -416,16 +411,46 @@ void Updater::slot_updateLinuxBinary()
|
|||
executablePermissions |= QFileDevice::ExeOwner | QFileDevice::ExeUser;
|
||||
|
||||
QDir dir;
|
||||
// dir.rename actually moves a file
|
||||
if (!(dir.remove(installedBinaryPath) && dir.rename(unzippedBinary.filePath(), installedBinaryPath))) {
|
||||
qWarning() << "updating" << installedBinaryPath << "with new version from" << unzippedBinary.filePath() << "failed";
|
||||
// Safely replace the old binary: rename old to backup first so we can
|
||||
// restore it if placing the new binary fails (e.g. cross-device rename)
|
||||
const QString backupPath = installedBinaryPath + qsl(".bak");
|
||||
if (!dir.remove(backupPath) && QFile::exists(backupPath)) {
|
||||
qWarning() << "Could not remove stale backup at" << backupPath;
|
||||
//: Error shown when the automatic update fails to install on Linux
|
||||
emit signal_updateCheckFailed(tr("Failed to install the update. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
if (!dir.rename(installedBinaryPath, backupPath)) {
|
||||
qWarning() << "could not back up old binary from" << installedBinaryPath << "to" << backupPath;
|
||||
//: Error shown when the automatic update fails to install on Linux
|
||||
emit signal_updateCheckFailed(tr("Failed to install the update. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
if (!dir.rename(unzippedBinary.filePath(), installedBinaryPath)) {
|
||||
qWarning() << "could not move new binary from" << unzippedBinary.filePath() << "to" << installedBinaryPath << "- restoring backup";
|
||||
if (!dir.rename(backupPath, installedBinaryPath)) {
|
||||
qWarning() << "could not restore backup from" << backupPath << "to" << installedBinaryPath;
|
||||
//: Error shown when the update fails and the previous version could not be restored automatically. %1 is the file path to the backup copy.
|
||||
emit signal_updateCheckFailed(tr("Failed to install the update and could not restore the previous version. "
|
||||
"Your previous version is saved at: %1 - please rename it back manually. "
|
||||
"Alternatively, download a fresh copy from https://www.mudlet.org/download/")
|
||||
.arg(backupPath));
|
||||
} else {
|
||||
//: Error shown when the automatic update fails to install on Linux
|
||||
emit signal_updateCheckFailed(tr("Failed to install the update. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!dir.remove(backupPath)) {
|
||||
qWarning() << "Could not clean up backup file:" << backupPath;
|
||||
}
|
||||
qWarning() << "successfully replaced old binary with new binary";
|
||||
|
||||
QFile updatedBinary(appimageLocation);
|
||||
if (!updatedBinary.setPermissions(executablePermissions)) {
|
||||
qWarning() << "couldn't set executable permissions on updated Mudlet binary at" << installedBinaryPath;
|
||||
//: Error shown when the automatic update fails to install on Linux
|
||||
emit signal_updateCheckFailed(tr("Failed to install the update. Please try again or download manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
qWarning() << "successfully set executable permissions for the new binary";
|
||||
|
|
@ -439,40 +464,43 @@ void Updater::slot_installOrRestartClicked(QAbstractButton* button, const QStrin
|
|||
{
|
||||
Q_UNUSED(button)
|
||||
|
||||
// moc, when used with cmake on macOS bugs out if the entire function declaration and definition is entirely
|
||||
// commented out so we leave a stub in
|
||||
// moc on macOS requires this function definition to exist even though macOS uses Sparkle instead
|
||||
#if !defined(Q_OS_MACOS)
|
||||
|
||||
// if the update is already installed, then the button says 'Restart' - do so
|
||||
if (mUpdateInstalled) {
|
||||
// timer is necessary as calling close right way doesn't seem to do the trick
|
||||
// defer to next event loop iteration so the dialog close happens after the button click handler returns
|
||||
QTimer::singleShot(0, this, [=, this]() {
|
||||
updateDialog->close();
|
||||
updateDialog->done(0);
|
||||
mUpdateDialog->close();
|
||||
mUpdateDialog->done(0);
|
||||
});
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
// On Windows, launch the installer directly with a delay to ensure Mudlet
|
||||
// has fully exited. This prevents "file in use" errors during the update.
|
||||
// The installer will relaunch Mudlet after the update completes.
|
||||
// On Windows, create and launch a batch file that waits for Mudlet to exit,
|
||||
// then runs the installer. This prevents "file in use" errors during the update.
|
||||
//: Error title for update-related warning dialogs
|
||||
const QString errorTitle = tr("Update Error");
|
||||
|
||||
if (mDownloadedInstallerPath.isEmpty() || !QFile::exists(mDownloadedInstallerPath)) {
|
||||
qWarning() << "Installer not found at:" << mDownloadedInstallerPath;
|
||||
QMessageBox::warning(nullptr, tr("Update Error"), tr("The update installer could not be found. Please try checking for updates again."));
|
||||
//: Error shown when the downloaded installer file cannot be found on disk
|
||||
QMessageBox::warning(nullptr, errorTitle, tr("The update installer could not be found. Please try checking for updates again."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the installer to a permanent location - the source is a QTemporaryFile
|
||||
// that will be deleted when Mudlet exits. We copy (not move) because AV
|
||||
// may still have a lock on the file, and copy only needs read access.
|
||||
// Copy the installer to a permanent location with a known name. We copy
|
||||
// (not move) because AV software may still have a lock on the file, and
|
||||
// copy only needs read access.
|
||||
// Use a unique filename with timestamp to avoid conflicts with locked files.
|
||||
QString installerPath = qsl("%1/mudlet-setup-%2.exe").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation)).arg(QDateTime::currentSecsSinceEpoch());
|
||||
if (!QFile::copy(mDownloadedInstallerPath, installerPath)) {
|
||||
qWarning() << "Failed to copy installer from" << mDownloadedInstallerPath << "to" << installerPath;
|
||||
QMessageBox::warning(nullptr, tr("Update Error"), tr("Could not prepare the update installer. Please try again or download the update manually from https://www.mudlet.org/download/"));
|
||||
//: Error shown when the installer file cannot be copied to a temporary location for launch
|
||||
QMessageBox::warning(nullptr, errorTitle, tr("Could not prepare the update installer. Please try again or download the update manually from https://www.mudlet.org/download/"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a batch file that waits for Mudlet and crashpad_handler to exit before launching installer
|
||||
// Create a batch file that waits for Mudlet to exit before launching installer
|
||||
// this avoids shell quoting issues that happen with QProcess::startDetached
|
||||
QString batchPath = qsl("%1/mudlet-update.bat").arg(QStandardPaths::writableLocation(QStandardPaths::TempLocation));
|
||||
QFile batchFile(batchPath);
|
||||
|
|
@ -497,44 +525,59 @@ void Updater::slot_installOrRestartClicked(QAbstractButton* button, const QStrin
|
|||
"\"%2\"\r\n"
|
||||
"echo Mudlet updater: installer finished with exit code %ERRORLEVEL%\r\n")
|
||||
.arg(exeName, QDir::toNativeSeparators(installerPath));
|
||||
batchFile.write(batchContent.toLocal8Bit());
|
||||
if (batchFile.write(batchContent.toLocal8Bit()) == -1) {
|
||||
qWarning() << "Failed to write update batch file:" << batchFile.errorString();
|
||||
//: Error shown when the batch file for managing the update process cannot be written. %1 is the path to the installer.
|
||||
QMessageBox::warning(nullptr, errorTitle, tr("Could not prepare the update. Please close Mudlet and run the installer manually:\n%1").arg(QDir::toNativeSeparators(installerPath)));
|
||||
return;
|
||||
}
|
||||
batchFile.close();
|
||||
|
||||
QProcess::startDetached(batchPath, QStringList());
|
||||
if (!QProcess::startDetached(batchPath, QStringList())) {
|
||||
qWarning() << "Failed to launch update batch file:" << batchPath;
|
||||
//: Error shown when the update installer process fails to start
|
||||
QMessageBox::warning(nullptr, errorTitle, tr("Could not launch the update installer. Please restart Mudlet and try again."));
|
||||
return;
|
||||
}
|
||||
qWarning() << "Launching installer via batch file:" << installerPath;
|
||||
} else {
|
||||
qWarning() << "Failed to create batch file, attempting direct launch";
|
||||
QProcess::startDetached(installerPath, QStringList());
|
||||
qWarning() << "Failed to create update batch file:" << batchFile.errorString();
|
||||
//: Error shown when the batch file for managing the update process cannot be created. %1 is the path to the installer.
|
||||
QMessageBox::warning(nullptr, errorTitle, tr("Could not prepare the update. Please close Mudlet and run the installer manually:\n%1").arg(QDir::toNativeSeparators(installerPath)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mudlet::self()) {
|
||||
mudlet::self()->forceClose();
|
||||
}
|
||||
// Don't restart Mudlet - the installer will do it after the update
|
||||
// Mudlet is not restarted here - the installer is expected to handle launching the updated version
|
||||
return;
|
||||
#else
|
||||
// if the updater is launched manually instead of when Mudlet is quit,
|
||||
// close Mudlet ourselves
|
||||
if (mudlet::self()) {
|
||||
mudlet::self()->forceClose();
|
||||
}
|
||||
QProcess::startDetached(qApp->arguments()[0], qApp->arguments());
|
||||
if (!QProcess::startDetached(qApp->arguments()[0], qApp->arguments())) {
|
||||
qWarning() << "Failed to restart Mudlet after update";
|
||||
//: Error title for dialog shown when Mudlet fails to restart after updating
|
||||
QMessageBox::critical(nullptr,
|
||||
tr("Update Error"),
|
||||
//: Error message shown when Mudlet fails to restart after updating on Linux
|
||||
tr("Could not restart Mudlet after the update. Please start it manually."));
|
||||
}
|
||||
return;
|
||||
#endif
|
||||
}
|
||||
|
||||
// otherwise the button says 'Install', so install the update
|
||||
#if defined(Q_OS_LINUX)
|
||||
QFuture<void> future = QtConcurrent::run([&, filePath]() {
|
||||
QFuture<void> future = QtConcurrent::run([this, filePath]() {
|
||||
untarOnLinux(filePath);
|
||||
});
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
QFuture<void> future = QtConcurrent::run([&, filePath]() {
|
||||
QFuture<void> future = QtConcurrent::run([this, filePath]() {
|
||||
prepareSetupOnWindows(filePath);
|
||||
});
|
||||
#endif
|
||||
|
||||
// replace current binary with the unzipped one
|
||||
auto watcher = new QFutureWatcher<void>;
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, [=, this]() {
|
||||
#if defined(Q_OS_LINUX)
|
||||
|
|
@ -542,7 +585,13 @@ void Updater::slot_installOrRestartClicked(QAbstractButton* button, const QStrin
|
|||
#elif defined(Q_OS_WINDOWS)
|
||||
finishSetup();
|
||||
#endif
|
||||
mpInstallOrRestart->setText(tr("Restart to apply update"));
|
||||
if (mUpdateInstalled) {
|
||||
//: Label for the button shown after the update has been downloaded and installed, prompting user to restart
|
||||
mpInstallOrRestart->setText(tr("Restart to apply update"));
|
||||
} else {
|
||||
//: Label for the update button shown when the update installation failed
|
||||
mpInstallOrRestart->setText(tr("Update failed"));
|
||||
}
|
||||
mpInstallOrRestart->setEnabled(true);
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
|
@ -550,11 +599,7 @@ void Updater::slot_installOrRestartClicked(QAbstractButton* button, const QStrin
|
|||
#endif // !Q_OS_MACOS
|
||||
}
|
||||
|
||||
// records a unix epoch on disk indicating that an update has happened.
|
||||
// Mudlet will use that on the next launch to decide whenever it should show
|
||||
// the window with the new features. The idea is that if you manually update (thus see the
|
||||
// changelog already) and restart, you shouldn't see it again, and if you automatically
|
||||
// updated, then you do want to see the changelog.
|
||||
// Records a timestamp on disk so shouldShowChangelog() can detect automatic updates on next launch
|
||||
void Updater::recordUpdateTime() const
|
||||
{
|
||||
QSaveFile file(mudlet::getMudletPath(enums::mainDataItemPath, qsl("mudlet_updated_at")));
|
||||
|
|
@ -564,13 +609,13 @@ void Updater::recordUpdateTime() const
|
|||
return;
|
||||
}
|
||||
|
||||
QDataStream ifs(&file);
|
||||
QDataStream ofs(&file);
|
||||
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
|
||||
ifs.setVersion(mudlet::scmQDataStreamFormat_5_12);
|
||||
ofs.setVersion(mudlet::scmQDataStreamFormat_5_12);
|
||||
}
|
||||
ifs << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
ofs << QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
if (!file.commit()) {
|
||||
qDebug() << "Updater::recordUpdateTime: error recording update time: " << file.errorString();
|
||||
qWarning() << "Updater::recordUpdateTime: error recording update time:" << file.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -585,22 +630,21 @@ void Updater::recordUpdatedVersion() const
|
|||
return;
|
||||
}
|
||||
|
||||
QDataStream ifs(&file);
|
||||
QDataStream ofs(&file);
|
||||
if (mudlet::scmRunTimeQtVersion >= QVersionNumber(5, 13, 0)) {
|
||||
ifs.setVersion(mudlet::scmQDataStreamFormat_5_12);
|
||||
ofs.setVersion(mudlet::scmQDataStreamFormat_5_12);
|
||||
}
|
||||
ifs << APP_VERSION;
|
||||
ofs << APP_VERSION;
|
||||
if (!file.commit()) {
|
||||
qDebug() << "Updater::recordUpdatedVersion: error saving old mudlet version: " << file.errorString();
|
||||
qWarning() << "Updater::recordUpdatedVersion: error saving old mudlet version:" << file.errorString();
|
||||
}
|
||||
}
|
||||
|
||||
// returns true if Mudlet was updated automatically and a changelog should be shown
|
||||
// now that the user is on the new version. If the user updated manually, then there
|
||||
// is no need as they would have seen the changelog while updating
|
||||
// Returns true if the changelog should be shown on this launch. Only applies to
|
||||
// non-development builds with auto-updates on non-macOS (Sparkle handles its own changelog).
|
||||
// Requires at least 5 minutes since the update to avoid re-showing a just-seen changelog.
|
||||
bool Updater::shouldShowChangelog()
|
||||
{
|
||||
// Don't show changelog for automatic updates on Sparkle - Sparkle doesn't support it
|
||||
#if defined(Q_OS_MACOS)
|
||||
return false;
|
||||
#endif
|
||||
|
|
@ -623,18 +667,20 @@ bool Updater::shouldShowChangelog()
|
|||
ifs >> updateTimestamp;
|
||||
file.close();
|
||||
|
||||
if (ifs.status() != QDataStream::Ok) {
|
||||
qWarning() << "Failed to read update timestamp file, treating as missing";
|
||||
file.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto currentDateTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
|
||||
auto minsSinceUpdate = (currentDateTime - updateTimestamp) / 1000 / 60;
|
||||
|
||||
// delete the file on check as well since if we updated and restarted right away
|
||||
// we won't need to show the changelog - as well as on a launch 5mins after.
|
||||
file.remove();
|
||||
|
||||
return minsSinceUpdate >= 5;
|
||||
}
|
||||
|
||||
// return the previous version of Mudlet that we updated from
|
||||
// return a null QString on failure
|
||||
QString Updater::getPreviousVersion() const
|
||||
{
|
||||
QFile file(mudlet::self()->getMudletPath(enums::mainDataItemPath, qsl("mudlet_updated_from")));
|
||||
|
|
@ -652,26 +698,10 @@ QString Updater::getPreviousVersion() const
|
|||
file.close();
|
||||
file.remove();
|
||||
|
||||
if (ifs.status() != QDataStream::Ok) {
|
||||
qWarning() << "Failed to read previous version file, treating as missing";
|
||||
return QString();
|
||||
}
|
||||
|
||||
return previousVersion;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
// we are trying to detect machines running a 32-Bit build of Mudlet on a 64-Bit Intel/AMD processor
|
||||
bool Updater::is64BitCompatible() const
|
||||
{
|
||||
#if defined(Q_OS_WIN64)
|
||||
return true;
|
||||
#endif
|
||||
|
||||
BOOL isWow64 = FALSE;
|
||||
typedef BOOL(WINAPI * LPFN_ISWOW64PROCESS)(HANDLE, PBOOL);
|
||||
LPFN_ISWOW64PROCESS fnIsWow64Process = (LPFN_ISWOW64PROCESS)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process");
|
||||
|
||||
if (fnIsWow64Process) {
|
||||
if (fnIsWow64Process(GetCurrentProcess(), &isWow64)) {
|
||||
return isWow64 ? true : false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -20,16 +20,25 @@
|
|||
#ifndef UPDATER_H
|
||||
#define UPDATER_H
|
||||
|
||||
// FreeBSD does not support the updater and these missing files upset
|
||||
// clang-tidy / Clazy when they are run in an environment without them:
|
||||
// QObject must be included before Q_OS_MACOS checks below
|
||||
#include <QObject>
|
||||
|
||||
// Guard for builds without the updater (INCLUDE_UPDATER is defined by CMake when USE_UPDATER is ON):
|
||||
#if defined(INCLUDE_UPDATER)
|
||||
#include "dblsqd/feed.h"
|
||||
#include "dblsqd/update_dialog.h"
|
||||
namespace dblsqd {
|
||||
class Feed;
|
||||
class Release;
|
||||
class UpdateDialog;
|
||||
}
|
||||
#if defined(Q_OS_MACOS)
|
||||
#include "sparkleupdater.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
#include <QObject>
|
||||
class QAbstractButton;
|
||||
class QPushButton;
|
||||
class QSettings;
|
||||
class QTimer;
|
||||
|
||||
class Updater : public QObject
|
||||
{
|
||||
|
|
@ -48,13 +57,15 @@ public:
|
|||
bool shouldShowChangelog();
|
||||
|
||||
private:
|
||||
dblsqd::Feed* feed;
|
||||
dblsqd::UpdateDialog* updateDialog{nullptr};
|
||||
dblsqd::Feed* mFeed;
|
||||
dblsqd::UpdateDialog* mUpdateDialog{nullptr};
|
||||
#if !defined(Q_OS_MACOS)
|
||||
QPushButton* mpInstallOrRestart;
|
||||
#endif
|
||||
bool mUpdateInstalled;
|
||||
bool mManualCheckInProgress{false};
|
||||
QSettings* settings;
|
||||
std::unique_ptr<QTimer> mDailyCheck;
|
||||
QSettings* mSettings;
|
||||
std::unique_ptr<QTimer> mPeriodicCheck;
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
void setupOnLinux();
|
||||
|
|
@ -62,11 +73,13 @@ private:
|
|||
#elif defined(Q_OS_WINDOWS)
|
||||
void setupOnWindows();
|
||||
void prepareSetupOnWindows(const QString& fileName);
|
||||
bool is64BitCompatible() const;
|
||||
#elif defined(Q_OS_MACOS)
|
||||
void setupOnMacOS();
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_MACOS)
|
||||
void setupPlatformUpdater();
|
||||
#endif
|
||||
void recordUpdateTime() const;
|
||||
void recordUpdatedVersion() const;
|
||||
QString getPreviousVersion() const;
|
||||
|
|
@ -75,7 +88,7 @@ private:
|
|||
void showDialogManually() const;
|
||||
|
||||
#if defined(Q_OS_LINUX)
|
||||
QString unzippedBinaryName;
|
||||
QString mUnzippedBinaryName;
|
||||
#elif defined(Q_OS_WINDOWS)
|
||||
QString mDownloadedInstallerPath;
|
||||
#elif defined(Q_OS_MACOS)
|
||||
|
|
@ -85,7 +98,6 @@ private:
|
|||
|
||||
signals:
|
||||
void signal_updateInstalled();
|
||||
// Argument is a count of updates available
|
||||
void signal_updateAvailable(const int);
|
||||
void signal_automaticUpdatesChanged(const bool);
|
||||
void signal_updateCheckFailed(const QString& error);
|
||||
|
|
@ -93,7 +105,6 @@ signals:
|
|||
public slots:
|
||||
void slot_installOrRestartClicked(QAbstractButton* button, const QString& filePath);
|
||||
#if defined(Q_OS_LINUX)
|
||||
// might want to make these private
|
||||
void slot_updateLinuxBinary();
|
||||
#endif
|
||||
};
|
||||
|
|
|
|||
434
src/updater/Feed.cpp
Normal file
434
src/updater/Feed.cpp
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include "Feed.h"
|
||||
|
||||
#include "../utils.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
Feed::Feed(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
Feed::~Feed()
|
||||
{
|
||||
cleanupDownloadFile();
|
||||
}
|
||||
|
||||
void Feed::setRepo(const QString& owner, const QString& repo, bool prerelease, const QString& os, const QString& arch)
|
||||
{
|
||||
mOwner = owner;
|
||||
mRepo = repo;
|
||||
mPrerelease = prerelease;
|
||||
mOs = os.isEmpty() ? detectOs() : os;
|
||||
mArch = arch.isEmpty() ? detectArch() : arch;
|
||||
|
||||
if (prerelease) {
|
||||
mUrl = QUrl(qsl("https://api.github.com/repos/%1/%2/releases?per_page=10").arg(owner, repo));
|
||||
} else {
|
||||
mUrl = QUrl(qsl("https://api.github.com/repos/%1/%2/releases?per_page=100").arg(owner, repo));
|
||||
}
|
||||
}
|
||||
|
||||
QString Feed::detectOs()
|
||||
{
|
||||
QString os = QSysInfo::productType().toLower();
|
||||
if (os == qsl("windows")) {
|
||||
return qsl("win");
|
||||
} else if (os == qsl("osx") || os == qsl("macos")) {
|
||||
return qsl("mac");
|
||||
}
|
||||
return QSysInfo::kernelType();
|
||||
}
|
||||
|
||||
QString Feed::detectArch()
|
||||
{
|
||||
QString autoArch = QSysInfo::buildCpuArchitecture();
|
||||
if (autoArch == qsl("i386") || autoArch == qsl("i586")) {
|
||||
return qsl("x86");
|
||||
}
|
||||
return autoArch;
|
||||
}
|
||||
|
||||
QUrl Feed::getUrl() const
|
||||
{
|
||||
return mUrl;
|
||||
}
|
||||
|
||||
QList<Release> Feed::getReleases() const
|
||||
{
|
||||
return mReleases;
|
||||
}
|
||||
|
||||
QList<Release> Feed::getUpdates(const Release& currentRelease) const
|
||||
{
|
||||
QList<Release> updates;
|
||||
for (const auto& release : mReleases) {
|
||||
if (currentRelease.getVersion().toLower() != release.getVersion().toLower() && currentRelease < release) {
|
||||
updates << release;
|
||||
}
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
QString Feed::getDownloadFilePath() const
|
||||
{
|
||||
return mDownloadFilePath;
|
||||
}
|
||||
|
||||
bool Feed::isReady() const
|
||||
{
|
||||
return mReady;
|
||||
}
|
||||
|
||||
void Feed::load()
|
||||
{
|
||||
if (mFeedReply != nullptr) {
|
||||
if (!mFeedReply->isFinished()) {
|
||||
qWarning() << "Update check already in progress, ignoring duplicate request";
|
||||
//: Error shown when the user triggers an update check while one is already running
|
||||
emit loadError(tr("Update check already in progress"));
|
||||
return;
|
||||
}
|
||||
mFeedReply->deleteLater();
|
||||
mFeedReply = nullptr;
|
||||
}
|
||||
|
||||
mReady = false;
|
||||
|
||||
QNetworkRequest request(getUrl());
|
||||
request.setRawHeader("Accept", "application/vnd.github+json");
|
||||
request.setRawHeader("User-Agent", "Mudlet-Updater");
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(30000);
|
||||
mFeedReply = mNam.get(request);
|
||||
connect(mFeedReply, &QNetworkReply::finished, this, &Feed::handleFeedFinished);
|
||||
}
|
||||
|
||||
void Feed::downloadRelease(const Release& release, bool requireChecksums)
|
||||
{
|
||||
const QUrl downloadUrl = release.getDownloadUrl();
|
||||
if (!downloadUrl.isValid() || downloadUrl.isEmpty()) {
|
||||
//: Error shown when the GitHub release has no binary matching the user's operating system
|
||||
emit downloadError(tr("No download available for your platform"));
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentDownload = release;
|
||||
mRequireChecksums = requireChecksums;
|
||||
const QUrl checksumsUrl = release.getChecksumsUrl();
|
||||
if (checksumsUrl.isValid() && !checksumsUrl.isEmpty()) {
|
||||
fetchChecksums(checksumsUrl);
|
||||
} else if (requireChecksums) {
|
||||
//: Error shown when a manual update cannot be verified as safe to install
|
||||
emit downloadError(tr("Could not verify the integrity of the download. Please try again later."));
|
||||
} else {
|
||||
makeDownloadRequest(downloadUrl);
|
||||
}
|
||||
}
|
||||
|
||||
void Feed::fetchChecksums(const QUrl& checksumsUrl)
|
||||
{
|
||||
QNetworkRequest request(checksumsUrl);
|
||||
request.setRawHeader("Accept", "application/octet-stream");
|
||||
request.setRawHeader("User-Agent", "Mudlet-Updater");
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(30000);
|
||||
auto* reply = mNam.get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
if (mRequireChecksums) {
|
||||
reply->deleteLater();
|
||||
//: Error shown when a manual update cannot be verified as safe to install
|
||||
emit downloadError(tr("Could not verify the integrity of the download. Please try again later."));
|
||||
return;
|
||||
}
|
||||
qWarning() << "Failed to fetch checksums:" << reply->errorString() << "- download will proceed without integrity verification";
|
||||
} else {
|
||||
const QString checksumData = QString::fromUtf8(reply->readAll());
|
||||
const QStringList lines = checksumData.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||||
// SHA256 hex digest is 64 characters; search for separator after that
|
||||
static const QRegularExpression separatorRx(qsl("[\\s*]+"));
|
||||
static const QRegularExpression hexRx(qsl("^[0-9a-fA-F]{64}$"));
|
||||
for (const auto& line : lines) {
|
||||
// Format: "hash filename" or "hash *filename"
|
||||
const int separatorPos = line.indexOf(separatorRx, 64);
|
||||
if (separatorPos <= 0) {
|
||||
continue;
|
||||
}
|
||||
const QString hash = line.left(separatorPos).trimmed();
|
||||
if (!hexRx.match(hash).hasMatch()) {
|
||||
continue;
|
||||
}
|
||||
const QString filename = line.mid(separatorPos).trimmed().remove(QLatin1Char('*'));
|
||||
|
||||
// Match against the download URL filename
|
||||
const QString downloadFilename = mCurrentDownload.getDownloadUrl().fileName();
|
||||
if (!downloadFilename.isEmpty() && filename.contains(downloadFilename, Qt::CaseInsensitive)) {
|
||||
mCurrentDownload.setDownloadSHA256(hash);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mCurrentDownload.getDownloadSHA256().isEmpty()) {
|
||||
if (mRequireChecksums) {
|
||||
reply->deleteLater();
|
||||
//: Error shown when a manual update cannot be verified as safe to install
|
||||
emit downloadError(tr("Could not verify the integrity of the download. Please try again later."));
|
||||
return;
|
||||
}
|
||||
qCritical() << "Checksum file downloaded but no matching hash found for" << mCurrentDownload.getDownloadUrl().fileName() << "- download will proceed without integrity verification";
|
||||
}
|
||||
}
|
||||
reply->deleteLater();
|
||||
makeDownloadRequest(mCurrentDownload.getDownloadUrl());
|
||||
});
|
||||
}
|
||||
|
||||
void Feed::makeDownloadRequest(const QUrl& url)
|
||||
{
|
||||
mDownloadFilePath.clear();
|
||||
|
||||
if (mDownloadReply != nullptr) {
|
||||
if (!mDownloadReply->isFinished()) {
|
||||
disconnect(mDownloadReply);
|
||||
mDownloadReply->abort();
|
||||
}
|
||||
mDownloadReply->deleteLater();
|
||||
mDownloadReply = nullptr;
|
||||
}
|
||||
if (mDownloadFile != nullptr) {
|
||||
mDownloadFile->close();
|
||||
mDownloadFile->deleteLater();
|
||||
mDownloadFile = nullptr;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setRawHeader("User-Agent", "Mudlet-Updater");
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(60000);
|
||||
mDownloadReply = mNam.get(request);
|
||||
connect(mDownloadReply, &QNetworkReply::downloadProgress, this, &Feed::downloadProgress);
|
||||
connect(mDownloadReply, &QNetworkReply::readyRead, this, &Feed::handleDownloadReadyRead);
|
||||
connect(mDownloadReply, &QNetworkReply::finished, this, &Feed::handleDownloadFinished);
|
||||
}
|
||||
|
||||
void Feed::handleFeedFinished()
|
||||
{
|
||||
if (mFeedReply->error() != QNetworkReply::NoError) {
|
||||
//: Error shown when the network request to the update server fails. %1 is the technical error description.
|
||||
emit loadError(tr("Could not connect to the update server: %1").arg(mFeedReply->errorString()));
|
||||
mFeedReply->deleteLater();
|
||||
mFeedReply = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
mReleases.clear();
|
||||
const QByteArray json = mFeedReply->readAll();
|
||||
mFeedReply->deleteLater();
|
||||
mFeedReply = nullptr;
|
||||
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json);
|
||||
if (doc.isNull()) {
|
||||
//: Error shown when the server response cannot be understood
|
||||
emit loadError(tr("Could not read update information from the server"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for GitHub API error responses (e.g. rate limiting, not found)
|
||||
// Both prerelease and release endpoints return an object with "message" on error
|
||||
if (doc.isObject() && doc.object().contains(qsl("message"))) {
|
||||
const QString apiMessage = doc.object().value(qsl("message")).toString();
|
||||
qWarning() << "GitHub API error:" << apiMessage;
|
||||
if (apiMessage.contains(qsl("rate limit"), Qt::CaseInsensitive)) {
|
||||
//: Error shown when the GitHub API rate limit has been exceeded
|
||||
emit loadError(tr("Update check temporarily unavailable. Please try again in a few minutes."));
|
||||
} else {
|
||||
//: Error shown when the GitHub API returns an error. %1 is the error message from the server.
|
||||
emit loadError(tr("Could not check for updates: %1").arg(apiMessage));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Both channels use the releases list endpoint and return an array
|
||||
if (!doc.isArray()) {
|
||||
//: Error shown when the update server response cannot be understood
|
||||
emit loadError(tr("Could not read update information from the server"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray releasesArray = doc.array();
|
||||
for (const auto& val : releasesArray) {
|
||||
const QJsonObject releaseObj = val.toObject();
|
||||
|
||||
// PTB channel: include only prereleases; stable channel: exclude prereleases
|
||||
if (mPrerelease != releaseObj.value(qsl("prerelease")).toBool()) {
|
||||
continue;
|
||||
}
|
||||
if (releaseObj.value(qsl("draft")).toBool()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Release rel(releaseObj, mOs, mArch);
|
||||
if (!rel.getVersion().isEmpty()) {
|
||||
mReleases << rel;
|
||||
} else {
|
||||
qWarning() << "Skipping release with empty version, tag_name:" << releaseObj.value(qsl("tag_name")).toString();
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(mReleases.begin(), mReleases.end(), [](const Release& a, const Release& b) {
|
||||
return b < a;
|
||||
});
|
||||
|
||||
mReady = true;
|
||||
emit ready();
|
||||
}
|
||||
|
||||
void Feed::handleDownloadReadyRead()
|
||||
{
|
||||
if (mDownloadFile == nullptr) {
|
||||
QString fileName = mDownloadReply->url().fileName();
|
||||
// handle compound extensions like .AppImage.tar when generating unique temp filenames
|
||||
static const QRegularExpression extensionRx(qsl("(?:\\.tar)?\\.[a-zA-Z0-9]+$"));
|
||||
int extensionPos = fileName.indexOf(extensionRx);
|
||||
if (extensionPos > -1) {
|
||||
fileName.insert(extensionPos, qsl("-XXXXXX"));
|
||||
}
|
||||
mDownloadFile = new QTemporaryFile(QDir::tempPath() + qsl("/") + fileName);
|
||||
if (!mDownloadFile->open()) {
|
||||
qWarning() << "Failed to create temporary file for download:" << mDownloadFile->errorString();
|
||||
//: Error shown when a temporary file cannot be created for the update download. %1 is the system error message.
|
||||
emit downloadError(tr("Could not create temporary file for download: %1").arg(mDownloadFile->errorString()));
|
||||
abortDownload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
const QByteArray data = mDownloadReply->readAll();
|
||||
const qint64 bytesWritten = mDownloadFile->write(data);
|
||||
if (bytesWritten != data.size()) {
|
||||
qWarning() << "Failed to write download data to temporary file:" << mDownloadFile->errorString();
|
||||
//: Error shown when writing download data to disk fails. %1 is the system error message.
|
||||
emit downloadError(tr("Failed to save download data: %1").arg(mDownloadFile->errorString()));
|
||||
abortDownload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Feed::handleDownloadFinished()
|
||||
{
|
||||
if (mDownloadReply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "Download failed:" << mDownloadReply->errorString() << "URL:" << mDownloadReply->url();
|
||||
//: Error shown when the update file download fails. %1 is the network error message.
|
||||
emit downloadError(tr("Download failed: %1").arg(mDownloadReply->errorString()));
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mDownloadFile == nullptr) {
|
||||
//: Error shown when the update download completed but nothing was received
|
||||
emit downloadError(tr("Download failed. Please try again."));
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mDownloadFile->flush()) {
|
||||
qWarning() << "Failed to flush download file:" << mDownloadFile->errorString();
|
||||
//: Error shown when flushing the downloaded file to disk fails. %1 is the system error message.
|
||||
emit downloadError(tr("Failed to save download: %1").arg(mDownloadFile->errorString()));
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
if (!mDownloadFile->seek(0)) {
|
||||
qWarning() << "Failed to seek in download file:" << mDownloadFile->errorString();
|
||||
//: Error shown when the downloaded file cannot be read back for checksum verification
|
||||
emit downloadError(tr("Failed to verify download integrity"));
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
QCryptographicHash fileHash(QCryptographicHash::Sha256);
|
||||
if (!fileHash.addData(mDownloadFile)) {
|
||||
qWarning() << "Failed to read download file for checksum verification:" << mDownloadFile->errorString();
|
||||
//: Error shown when the downloaded file cannot be read back for checksum verification
|
||||
emit downloadError(tr("Failed to verify download integrity"));
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
const QString hashResult = fileHash.result().toHex();
|
||||
if (!mCurrentDownload.getDownloadSHA256().isEmpty() && hashResult.toLower() != mCurrentDownload.getDownloadSHA256().toLower()) {
|
||||
qWarning() << "SHA256 mismatch - expected:" << mCurrentDownload.getDownloadSHA256() << "got:" << hashResult;
|
||||
//: Error shown when the downloaded file's SHA256 checksum does not match the expected value
|
||||
emit downloadError(tr("Could not verify download integrity."));
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
return;
|
||||
}
|
||||
|
||||
mDownloadFile->setAutoRemove(false);
|
||||
mDownloadFilePath = mDownloadFile->fileName();
|
||||
cleanupDownloadFile();
|
||||
cleanupDownloadReply();
|
||||
emit downloadFinished();
|
||||
}
|
||||
|
||||
void Feed::abortDownload()
|
||||
{
|
||||
if (mDownloadReply) {
|
||||
disconnect(mDownloadReply, &QNetworkReply::finished, this, &Feed::handleDownloadFinished);
|
||||
mDownloadReply->abort();
|
||||
mDownloadReply->deleteLater();
|
||||
mDownloadReply = nullptr;
|
||||
}
|
||||
cleanupDownloadFile();
|
||||
}
|
||||
|
||||
void Feed::cleanupDownloadReply()
|
||||
{
|
||||
if (mDownloadReply) {
|
||||
mDownloadReply->deleteLater();
|
||||
mDownloadReply = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Feed::cleanupDownloadFile()
|
||||
{
|
||||
if (mDownloadFile) {
|
||||
mDownloadFile->close();
|
||||
delete mDownloadFile;
|
||||
mDownloadFile = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dblsqd
|
||||
100
src/updater/Feed.h
Normal file
100
src/updater/Feed.h
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef DBLSQD_FEED_H
|
||||
#define DBLSQD_FEED_H
|
||||
|
||||
#include "Release.h"
|
||||
|
||||
#include <QList>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
class QNetworkReply;
|
||||
class QTemporaryFile;
|
||||
|
||||
// Namespace retained from the previous dblsqd update system to minimize changes in dependent code
|
||||
namespace dblsqd {
|
||||
|
||||
class Feed : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Feed(QObject* parent = nullptr);
|
||||
~Feed();
|
||||
|
||||
void setRepo(const QString& owner, const QString& repo, bool prerelease = false, const QString& os = QString(), const QString& arch = QString());
|
||||
QUrl getUrl() const;
|
||||
|
||||
void load();
|
||||
void downloadRelease(const Release& release, bool requireChecksums = false);
|
||||
|
||||
QList<Release> getUpdates(const Release& currentRelease) const;
|
||||
QList<Release> getReleases() const;
|
||||
QString getDownloadFilePath() const;
|
||||
bool isReady() const;
|
||||
|
||||
signals:
|
||||
void ready();
|
||||
void loadError(const QString& message);
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
void downloadFinished();
|
||||
void downloadError(const QString& message);
|
||||
|
||||
private:
|
||||
QUrl mUrl;
|
||||
|
||||
QList<Release> mReleases;
|
||||
|
||||
void makeDownloadRequest(const QUrl& url);
|
||||
void fetchChecksums(const QUrl& checksumsUrl);
|
||||
void abortDownload();
|
||||
void cleanupDownloadReply();
|
||||
void cleanupDownloadFile();
|
||||
bool mRequireChecksums{false};
|
||||
|
||||
QNetworkAccessManager mNam;
|
||||
QNetworkReply* mFeedReply{nullptr};
|
||||
Release mCurrentDownload;
|
||||
QNetworkReply* mDownloadReply{nullptr};
|
||||
QTemporaryFile* mDownloadFile{nullptr};
|
||||
QString mDownloadFilePath;
|
||||
bool mReady{false};
|
||||
|
||||
QString mOwner;
|
||||
QString mRepo;
|
||||
bool mPrerelease{false};
|
||||
QString mOs;
|
||||
QString mArch;
|
||||
|
||||
static QString detectOs();
|
||||
static QString detectArch();
|
||||
|
||||
private slots:
|
||||
void handleFeedFinished();
|
||||
void handleDownloadReadyRead();
|
||||
void handleDownloadFinished();
|
||||
};
|
||||
|
||||
} // namespace dblsqd
|
||||
|
||||
#endif // DBLSQD_FEED_H
|
||||
201
src/updater/Release.cpp
Normal file
201
src/updater/Release.cpp
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include "Release.h"
|
||||
|
||||
#include "SemVer.h"
|
||||
|
||||
#include "../utils.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
/*!
|
||||
* \class Release
|
||||
* \brief This class is used to represent information about a single Release
|
||||
* from a Feed.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new Release from a GitHub Releases API JSON object.
|
||||
*/
|
||||
Release::Release(const QJsonObject& releaseInfo, const QString& os, const QString& arch)
|
||||
{
|
||||
const QString tagName = releaseInfo.value(qsl("tag_name")).toString();
|
||||
if (tagName.isEmpty()) {
|
||||
qWarning() << "Release missing tag_name field";
|
||||
}
|
||||
mVersion = tagName.startsWith(qsl("Mudlet-")) ? tagName.mid(7) : tagName;
|
||||
|
||||
mDate = QDateTime::fromString(releaseInfo.value(qsl("published_at")).toString(), Qt::ISODate);
|
||||
if (!mDate.isValid()) {
|
||||
qWarning() << "Release" << mVersion << "has invalid or missing published_at date";
|
||||
}
|
||||
|
||||
const QString body = releaseInfo.value(qsl("body")).toString();
|
||||
if (!body.isEmpty()) {
|
||||
mChangelog = body;
|
||||
}
|
||||
|
||||
const QJsonArray assets = releaseInfo.value(qsl("assets")).toArray();
|
||||
const QString assetPattern = buildAssetPattern(os, arch);
|
||||
|
||||
for (const auto& assetVal : assets) {
|
||||
const QJsonObject asset = assetVal.toObject();
|
||||
const QString name = asset.value(qsl("name")).toString();
|
||||
|
||||
if (name == qsl("SHA256SUMS.txt")) {
|
||||
mChecksumsUrl = QUrl(asset.value(qsl("browser_download_url")).toString());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mDownloadUrl.isEmpty() && !assetPattern.isEmpty() && name.contains(assetPattern, Qt::CaseInsensitive)) {
|
||||
mDownloadUrl = QUrl(asset.value(qsl("browser_download_url")).toString());
|
||||
mDownloadSize = static_cast<qint64>(asset.value(qsl("size")).toDouble());
|
||||
}
|
||||
}
|
||||
|
||||
if (mDownloadUrl.isEmpty() && !mVersion.isEmpty() && !os.isEmpty()) {
|
||||
qWarning() << "No matching asset found for" << os << arch << "in release" << mVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new Release from a version string and a date.
|
||||
*
|
||||
* This method is useful when constructing a "virtual" Release for comparing
|
||||
* it with Releases retrieved from a Feed.
|
||||
*/
|
||||
Release::Release(const QString& version, const QDateTime& date)
|
||||
: mVersion(version)
|
||||
, mDate(date)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator<(const Release& one, const Release& other)
|
||||
{
|
||||
SemVer v1(one.mVersion);
|
||||
SemVer v2(other.mVersion);
|
||||
|
||||
// Both valid SemVer: use semantic version comparison
|
||||
if (v1.isValid() && v2.isValid()) {
|
||||
return (v1 < v2);
|
||||
}
|
||||
|
||||
// Mixed validity: valid SemVer sorts after invalid to ensure total ordering
|
||||
// (required by std::sort's strict weak ordering contract)
|
||||
if (v1.isValid() != v2.isValid()) {
|
||||
return v2.isValid();
|
||||
}
|
||||
|
||||
// Both invalid SemVer (e.g. PTB releases): fall back to date comparison
|
||||
return (one.mDate < other.mDate);
|
||||
}
|
||||
|
||||
bool operator==(const Release& one, const Release& other)
|
||||
{
|
||||
SemVer v1(one.mVersion);
|
||||
SemVer v2(other.mVersion);
|
||||
|
||||
if (v1.isValid() && v2.isValid()) {
|
||||
return v1 == v2;
|
||||
}
|
||||
|
||||
return one.mVersion.compare(other.mVersion, Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
|
||||
bool operator<=(const Release& one, const Release& other)
|
||||
{
|
||||
return one == other || one < other;
|
||||
}
|
||||
|
||||
QString Release::getVersion() const
|
||||
{
|
||||
return mVersion;
|
||||
}
|
||||
|
||||
QString Release::getChangelog() const
|
||||
{
|
||||
return mChangelog;
|
||||
}
|
||||
|
||||
QDateTime Release::getDate() const
|
||||
{
|
||||
return mDate;
|
||||
}
|
||||
|
||||
QUrl Release::getDownloadUrl() const
|
||||
{
|
||||
return mDownloadUrl;
|
||||
}
|
||||
|
||||
QString Release::getDownloadSHA256() const
|
||||
{
|
||||
return mDownloadSHA256;
|
||||
}
|
||||
|
||||
void Release::setDownloadSHA256(const QString& sha256)
|
||||
{
|
||||
mDownloadSHA256 = sha256;
|
||||
}
|
||||
|
||||
qint64 Release::getDownloadSize() const
|
||||
{
|
||||
return mDownloadSize;
|
||||
}
|
||||
|
||||
QUrl Release::getChecksumsUrl() const
|
||||
{
|
||||
return mChecksumsUrl;
|
||||
}
|
||||
|
||||
dblsqd::Release Release::getCurrentRelease()
|
||||
{
|
||||
// embed build time so public test releases, which cannot be compared via semver, can be compared via datetime
|
||||
QString buildDateTime = QString(__DATE__) + " " + QString(__TIME__);
|
||||
// locale-independent datetime parsing (C locale matches __DATE__'s English format)
|
||||
QDateTime date = QLocale::c().toDateTime(buildDateTime.simplified(), qsl("MMM d yyyy hh:mm:ss"));
|
||||
|
||||
return dblsqd::Release(QCoreApplication::applicationVersion(), date);
|
||||
}
|
||||
|
||||
QString Release::buildAssetPattern(const QString& os, const QString& arch)
|
||||
{
|
||||
if (os == qsl("linux")) {
|
||||
return qsl("-linux-x64.AppImage.tar");
|
||||
} else if (os == qsl("win")) {
|
||||
return qsl("-windows-64");
|
||||
} else if (os == qsl("mac")) {
|
||||
if (arch == qsl("arm64") || arch == qsl("aarch64")) {
|
||||
return qsl("-arm64.dmg");
|
||||
}
|
||||
return qsl("-x86_64.dmg");
|
||||
}
|
||||
if (!os.isEmpty()) {
|
||||
qWarning() << "No asset pattern defined for OS:" << os << "arch:" << arch;
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
} // namespace dblsqd
|
||||
66
src/updater/Release.h
Normal file
66
src/updater/Release.h
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef DBLSQD_RELEASE_H
|
||||
#define DBLSQD_RELEASE_H
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
class QJsonObject;
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
class Release
|
||||
{
|
||||
public:
|
||||
explicit Release(const QJsonObject& releaseInfo, const QString& os = QString(), const QString& arch = QString());
|
||||
explicit Release(const QString& version = QString(), const QDateTime& date = QDateTime());
|
||||
|
||||
friend bool operator<(const Release& one, const Release& other);
|
||||
friend bool operator==(const Release& one, const Release& other);
|
||||
friend bool operator<=(const Release& one, const Release& other);
|
||||
|
||||
QString getVersion() const;
|
||||
QString getChangelog() const;
|
||||
QDateTime getDate() const;
|
||||
QUrl getDownloadUrl() const;
|
||||
QString getDownloadSHA256() const;
|
||||
qint64 getDownloadSize() const;
|
||||
QUrl getChecksumsUrl() const;
|
||||
void setDownloadSHA256(const QString& sha256);
|
||||
static dblsqd::Release getCurrentRelease();
|
||||
|
||||
private:
|
||||
QString mVersion;
|
||||
QDateTime mDate;
|
||||
QString mChangelog;
|
||||
QUrl mDownloadUrl;
|
||||
qint64 mDownloadSize{0};
|
||||
QString mDownloadSHA256;
|
||||
QUrl mChecksumsUrl;
|
||||
|
||||
static QString buildAssetPattern(const QString& os, const QString& arch);
|
||||
};
|
||||
|
||||
} // namespace dblsqd
|
||||
|
||||
#endif // DBLSQD_RELEASE_H
|
||||
119
src/updater/SemVer.cpp
Normal file
119
src/updater/SemVer.cpp
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include "SemVer.h"
|
||||
|
||||
#include "../utils.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
/*!
|
||||
* \class SemVer
|
||||
* \brief SemVer encapsulates a version according to
|
||||
* Semantic Versioning 2.0.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new SemVer object from a string.
|
||||
*/
|
||||
SemVer::SemVer(const QString& version)
|
||||
{
|
||||
static const QRegularExpression rx(getRegExp());
|
||||
QRegularExpressionMatch match = rx.match(version);
|
||||
if (match.hasMatch()) {
|
||||
mMajor = match.captured(1).toInt();
|
||||
mMinor = match.captured(2).toInt();
|
||||
mPatch = match.captured(3).toInt();
|
||||
mPrerelease = match.captured(4);
|
||||
mValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns true if this version is valid according to the SemVer
|
||||
* specification. Otherwise returns false.
|
||||
*/
|
||||
bool SemVer::isValid() const
|
||||
{
|
||||
return mValid;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Compares two SemVer objects.
|
||||
*
|
||||
* Returns true if the left-hand SemVer object represents a lower version
|
||||
* according to the SemVer 2.0 specification.
|
||||
* Otherwise returns false.
|
||||
* Returns false if one of the SemVer objects does not represent a valid
|
||||
* SemVer.
|
||||
* \sa isValid()
|
||||
*/
|
||||
bool SemVer::operator<(const SemVer& other) const
|
||||
{
|
||||
if (!isValid() || !other.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mMajor != other.mMajor) {
|
||||
return mMajor < other.mMajor;
|
||||
} else if (mMinor != other.mMinor) {
|
||||
return mMinor < other.mMinor;
|
||||
} else if (mPatch != other.mPatch) {
|
||||
return mPatch < other.mPatch;
|
||||
} else if (mPrerelease != other.mPrerelease) {
|
||||
if (mPrerelease.isEmpty()) {
|
||||
return false;
|
||||
} else if (other.mPrerelease.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
// Simplified: compares the whole prerelease string lexicographically.
|
||||
// SemVer 2.0 §11.4 specifies splitting on '.' and comparing numeric
|
||||
// segments as integers, but Mudlet only uses simple prerelease tags.
|
||||
return (QString::compare(mPrerelease, other.mPrerelease) < 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns true if both SemVer objects represent the same version.
|
||||
*
|
||||
* Build metadata is ignored per SemVer 2.0 spec.
|
||||
* Returns false if either object is invalid.
|
||||
*/
|
||||
bool SemVer::operator==(const SemVer& other) const
|
||||
{
|
||||
if (!isValid() || !other.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return mMajor == other.mMajor && mMinor == other.mMinor && mPatch == other.mPatch && mPrerelease == other.mPrerelease;
|
||||
}
|
||||
|
||||
QString SemVer::getRegExp()
|
||||
{
|
||||
QString v = qsl("(0|[1-9]\\d*)");
|
||||
QString p = qsl("(?:-((?:0|[1-9A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*))?");
|
||||
QString b = qsl("(?:\\+((?:[0-9A-Za-z-]*)(?:\\.(?:[0-9A-Za-z-][0-9A-Za-z-]*))*))?");
|
||||
return qsl("^") + v + qsl("\\.") + v + qsl("\\.") + v + p + b + qsl("$");
|
||||
}
|
||||
|
||||
} // namespace dblsqd
|
||||
50
src/updater/SemVer.h
Normal file
50
src/updater/SemVer.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef DBLSQD_SEMVER_H
|
||||
#define DBLSQD_SEMVER_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
class SemVer
|
||||
{
|
||||
public:
|
||||
explicit SemVer(const QString& version);
|
||||
|
||||
bool operator<(const SemVer& other) const;
|
||||
bool operator==(const SemVer& other) const;
|
||||
|
||||
bool isValid() const;
|
||||
|
||||
private:
|
||||
int mMajor{0};
|
||||
int mMinor{0};
|
||||
int mPatch{0};
|
||||
QString mPrerelease;
|
||||
bool mValid{false};
|
||||
|
||||
static QString getRegExp();
|
||||
};
|
||||
|
||||
} // namespace dblsqd
|
||||
|
||||
#endif // DBLSQD_SEMVER_H
|
||||
704
src/updater/UpdateDialog.cpp
Normal file
704
src/updater/UpdateDialog.cpp
Normal file
|
|
@ -0,0 +1,704 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#include "UpdateDialog.h"
|
||||
#include "Feed.h"
|
||||
#include "ui_update_dialog.h"
|
||||
|
||||
#include "../utils.h"
|
||||
#include "../../3rdparty/kdtoolbox/singleshot_connect/singleshot_connect.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDesktopServices>
|
||||
#include <QFile>
|
||||
#include <QGuiApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QPixmap>
|
||||
#include <QSettings>
|
||||
#include <QTextBrowser>
|
||||
#include <QToolButton>
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
/*!
|
||||
* \class UpdateDialog
|
||||
* \brief A dialog class for displaying and downloading update information.
|
||||
*
|
||||
* UpdateDialog displays available updates from the GitHub Releases feed
|
||||
* and provides download/install functionality.
|
||||
*
|
||||
* The update dialog can also display an application icon which can be set with
|
||||
* setIcon().
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \enum UpdateDialog::Type
|
||||
* \brief This flag determines the if and when the UpdateDialog is displayed
|
||||
* automatically.
|
||||
*
|
||||
* *OnUpdateAvailable*: Automatically display the dialog as soon as the Feed
|
||||
* has been downloaded and parsed and if there is a newer version than the
|
||||
* current version returned by QCoreApplication::applicationVersion().
|
||||
*
|
||||
* *OnLastWindowClosed*: If there is a newer version available than the current
|
||||
* version returned by QCoreApplication::applicationVersion(), the update
|
||||
* dialog is displayed when QGuiApplication emits the lastWindowClosed() event.
|
||||
* Note that when this flag is used,
|
||||
* QGuiApplication::setQuitOnLastWindowClosed(false) will be called.
|
||||
*
|
||||
* *Manual*: The dialog is only displayed when explicitly requested via show()
|
||||
* or exec().
|
||||
* Note that update information might not be available instantly after
|
||||
* constructing an UpdateDialog.
|
||||
*
|
||||
* *ManualChangelog*: The dialog is only displayed when explicitly requested via
|
||||
* show() or exec().
|
||||
* Instead of the full update interface, only the changelog will be shown.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* \brief Constructs a new UpdateDialog.
|
||||
*
|
||||
* A Feed object needs to be constructed first and passed to this constructor.
|
||||
* Feed::load() does not need to be called before constructing this dialog --
|
||||
* the constructor calls it automatically if needed.
|
||||
*
|
||||
* The given UpdateDialog::Type flag determines when/if the dialog is shown
|
||||
* automatically.
|
||||
*
|
||||
* A QSettings object must be provided for persisting user preferences such as
|
||||
* skipped releases and auto-download settings.
|
||||
*
|
||||
*/
|
||||
UpdateDialog::UpdateDialog(Feed* feed, Type type, QSettings* settings, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, mUi(new Ui::UpdateDialog)
|
||||
, mFeed(feed)
|
||||
, mType(type)
|
||||
, mSettings(settings)
|
||||
, mAcceptedInstallButton(nullptr)
|
||||
{
|
||||
Q_ASSERT_X(feed, "UpdateDialog", "Feed object is required");
|
||||
Q_ASSERT_X(settings, "UpdateDialog", "QSettings object is required");
|
||||
mUi->setupUi(this);
|
||||
|
||||
mUi->buttonCancel->addAction(mUi->actionCancel);
|
||||
mUi->buttonCancel->addAction(mUi->actionSkip);
|
||||
mUi->buttonCancel->setDefaultAction(mUi->actionCancel);
|
||||
|
||||
connect(mUi->labelChangelog, &QTextBrowser::anchorClicked, this, &UpdateDialog::onLinkActivated);
|
||||
|
||||
switch (mType) {
|
||||
case OnUpdateAvailable: {
|
||||
connect(this, &UpdateDialog::ready, this, &UpdateDialog::showIfUpdatesAvailable);
|
||||
break;
|
||||
}
|
||||
case OnLastWindowClosed: {
|
||||
auto* app = qobject_cast<QGuiApplication*>(QApplication::instance());
|
||||
app->setQuitOnLastWindowClosed(false);
|
||||
connect(app, &QGuiApplication::lastWindowClosed, this, &UpdateDialog::showIfUpdatesAvailableOrQuit);
|
||||
break;
|
||||
}
|
||||
case Manual:
|
||||
case ManualChangelog:
|
||||
break;
|
||||
}
|
||||
|
||||
if (mFeed->isReady()) {
|
||||
handleFeedReady();
|
||||
} else {
|
||||
setupLoadingUi();
|
||||
mFeed->load();
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::ready, this, &UpdateDialog::handleFeedReady);
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::loadError, this, &UpdateDialog::handleLoadError);
|
||||
}
|
||||
}
|
||||
|
||||
UpdateDialog::~UpdateDialog()
|
||||
{
|
||||
delete mUi;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the icon displayed in the update window.
|
||||
*/
|
||||
void UpdateDialog::setIcon(const QPixmap& pixmap)
|
||||
{
|
||||
mUi->labelIcon->setPixmap(pixmap);
|
||||
mUi->labelIcon->setHidden(false);
|
||||
}
|
||||
|
||||
void UpdateDialog::setIcon(const QString& fileName)
|
||||
{
|
||||
mUi->labelIcon->setPixmap(QPixmap(fileName));
|
||||
mUi->labelIcon->setHidden(false);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the minimum version to be displayed in the changelog.
|
||||
* Defaults to QApplication::applicationVersion() if not set.
|
||||
* \param version
|
||||
*/
|
||||
void UpdateDialog::setMinVersion(const QString& version)
|
||||
{
|
||||
mMinVersion = version;
|
||||
setupChangelogUi();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets the maximum version to be displayed in the changelog.
|
||||
*/
|
||||
void UpdateDialog::setMaxVersion(const QString& version)
|
||||
{
|
||||
mMaxVersion = version;
|
||||
setupChangelogUi();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Convenience method for setting minimum and maximum version to be
|
||||
* displayed in the changelog. maximumVersion is set to
|
||||
* QApplication::applicationVersion().
|
||||
*/
|
||||
void UpdateDialog::setPreviousVersion(const QString& previousVersion)
|
||||
{
|
||||
mPreviousVersion = previousVersion;
|
||||
mMinVersion = previousVersion;
|
||||
mMaxVersion = QApplication::applicationVersion();
|
||||
setupChangelogUi();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Adds a custom button for handling update installation.
|
||||
* \param button
|
||||
*
|
||||
* When the custom button is clicked after an update has been downloaded or when
|
||||
* downloading an update that was started by clicking the button has finished,
|
||||
* installButtonClicked(QAbstractButton* button, QString filePath) is emitted.
|
||||
*/
|
||||
void UpdateDialog::addInstallButton(QAbstractButton* button)
|
||||
{
|
||||
mInstallButtons.append(button);
|
||||
mUi->buttonContainer->layout()->addWidget(button);
|
||||
if (isVisible() && mUi->buttonCancel->isVisible()) {
|
||||
setupUpdateUi();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns whether links in the changelog are opened externally.
|
||||
*
|
||||
* Determines if links in the changelog should be opened automatically by
|
||||
* QDesktopServices::openUrl() when a user clicks on them.
|
||||
* If set to false, the linkActivated() signal is emitted instead.
|
||||
*
|
||||
* The default value is true.
|
||||
*/
|
||||
bool UpdateDialog::openExternalLinks() const
|
||||
{
|
||||
return mOpenExternalLinks;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Sets whether links in the changelog are opened externally.
|
||||
*/
|
||||
void UpdateDialog::setOpenExternalLinks(bool open)
|
||||
{
|
||||
mOpenExternalLinks = open;
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Default handler for the install button.
|
||||
*
|
||||
* Closes the dialog if no other action (such as
|
||||
* downloading or installing a Release) is required first.
|
||||
*/
|
||||
void UpdateDialog::onButtonInstall()
|
||||
{
|
||||
mAccepted = true;
|
||||
mAcceptedInstallButton = nullptr;
|
||||
if (mIsDownloadFinished) {
|
||||
startUpdate();
|
||||
} else if (!mLatestRelease.getVersion().isEmpty()) {
|
||||
startDownload();
|
||||
} else {
|
||||
done(QDialog::Accepted);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::onButtonCustomInstall()
|
||||
{
|
||||
mAccepted = true;
|
||||
if (mIsDownloadFinished) {
|
||||
emit installButtonClicked(qobject_cast<QAbstractButton*>(sender()), mUpdateFilePath);
|
||||
} else if (!mLatestRelease.getVersion().isEmpty()) {
|
||||
mAcceptedInstallButton = qobject_cast<QAbstractButton*>(sender());
|
||||
startDownload();
|
||||
} else {
|
||||
done(QDialog::Accepted);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Skips the latest retrieved Release.
|
||||
*
|
||||
* If a release has been skipped, UpdateDialog will not be displayed
|
||||
* automatically when using Type::OnUpdateAvailable or
|
||||
* Type::OnLastWindowClosed.
|
||||
*/
|
||||
void UpdateDialog::skip()
|
||||
{
|
||||
if (!mUpdateFilePath.isEmpty()) {
|
||||
if (!QFile::remove(mUpdateFilePath)) {
|
||||
qWarning() << "Failed to remove update file:" << mUpdateFilePath;
|
||||
}
|
||||
}
|
||||
setSettingsValue(qsl("skipRelease"), mLatestRelease.getVersion(), mSettings);
|
||||
done(QDialog::Rejected);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Shows the dialog if there are available updates.
|
||||
*/
|
||||
void UpdateDialog::showIfUpdatesAvailable()
|
||||
{
|
||||
QString latestVersion = mLatestRelease.getVersion();
|
||||
bool skipRelease = (settingsValue(qsl("skipRelease"), "", mSettings).toString() == latestVersion);
|
||||
if (!latestVersion.isEmpty() && !skipRelease) {
|
||||
show();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Shows the dialog if there are updates available or quits the
|
||||
* application.
|
||||
*/
|
||||
void UpdateDialog::showIfUpdatesAvailableOrQuit()
|
||||
{
|
||||
if (mType == OnLastWindowClosed) {
|
||||
auto* app = qobject_cast<QGuiApplication*>(QApplication::instance());
|
||||
app->setQuitOnLastWindowClosed(true);
|
||||
disconnect(app, &QGuiApplication::lastWindowClosed, this, &UpdateDialog::showIfUpdatesAvailableOrQuit);
|
||||
}
|
||||
QString latestVersion = mLatestRelease.getVersion();
|
||||
bool skipRelease = (settingsValue(qsl("skipRelease"), "", mSettings).toString() == latestVersion);
|
||||
if (!latestVersion.isEmpty() && !skipRelease) {
|
||||
show();
|
||||
} else {
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
}
|
||||
|
||||
// "DBLSQD/" prefix retained for backward compatibility with user settings from the previous update system
|
||||
QVariant UpdateDialog::settingsValue(const QString& key, const QVariant& defaultValue, QSettings* settings)
|
||||
{
|
||||
return settings->value(qsl("DBLSQD/") + key, defaultValue);
|
||||
}
|
||||
|
||||
void UpdateDialog::setSettingsValue(const QString& key, const QVariant& value, QSettings* settings)
|
||||
{
|
||||
settings->setValue(qsl("DBLSQD/") + key, value);
|
||||
}
|
||||
|
||||
void UpdateDialog::removeSetting(const QString& key, QSettings* settings)
|
||||
{
|
||||
settings->remove(qsl("DBLSQD/") + key);
|
||||
}
|
||||
|
||||
void UpdateDialog::setDefaultSettingsValue(const QString& key, const QVariant& value, QSettings* settings)
|
||||
{
|
||||
if (settings->contains(qsl("DBLSQD/") + key))
|
||||
return;
|
||||
setSettingsValue(key, value, settings);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Enables or disables automatic downloads.
|
||||
*/
|
||||
void UpdateDialog::enableAutoDownload(bool enabled, QSettings* settings)
|
||||
{
|
||||
setSettingsValue(qsl("autoDownload"), enabled, settings);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns true if automatic downloads are enabled.
|
||||
*
|
||||
* If defaultValue is provided, it is stored if no other value has previously
|
||||
* been set.
|
||||
*/
|
||||
bool UpdateDialog::autoDownloadEnabled(QVariant defaultValue, QSettings* settings)
|
||||
{
|
||||
if (defaultValue.isValid()) {
|
||||
setDefaultSettingsValue(qsl("autoDownload"), defaultValue, settings);
|
||||
} else {
|
||||
defaultValue = false;
|
||||
}
|
||||
return settingsValue(qsl("autoDownload"), defaultValue, settings).toBool();
|
||||
}
|
||||
|
||||
/*!
|
||||
* \overload
|
||||
*/
|
||||
bool UpdateDialog::autoDownloadEnabled(QSettings* settings)
|
||||
{
|
||||
return settingsValue(qsl("autoDownload"), false, settings).toBool();
|
||||
}
|
||||
|
||||
void UpdateDialog::adjustDialogSize()
|
||||
{
|
||||
adjustSize();
|
||||
|
||||
/*HACK: Qt seems to incorrectly calculate window geometry on Windows.
|
||||
This code avoids warning messages logged by the application
|
||||
in that case.*/
|
||||
#if defined(Q_OS_WINDOWS)
|
||||
QSize dialogSize = size();
|
||||
resize(dialogSize.width(), dialogSize.height() + 3);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateDialog::updateWindowTitle()
|
||||
{
|
||||
QString title = windowTitle();
|
||||
replaceAppVars(title);
|
||||
setWindowTitle(title);
|
||||
}
|
||||
|
||||
void UpdateDialog::resetUi()
|
||||
{
|
||||
QList<QWidget*> hiddenWidgets;
|
||||
for (auto* button : mInstallButtons) {
|
||||
hiddenWidgets << button;
|
||||
}
|
||||
hiddenWidgets << mUi->headerContainer << mUi->labelIcon << mUi->headerContainerLoading << mUi->headerContainerNoUpdates << mUi->headerContainerChangelog << mUi->labelChangelog << mUi->progressBar
|
||||
<< mUi->checkAutoDownload << mUi->buttonCancel << mUi->buttonCancelLoading << mUi->buttonConfirm << mUi->buttonInstall;
|
||||
for (auto* widget : hiddenWidgets) {
|
||||
widget->hide();
|
||||
widget->disconnect();
|
||||
}
|
||||
// Re-establish the changelog link handler broken by disconnect() above
|
||||
connect(mUi->labelChangelog, &QTextBrowser::anchorClicked, this, &UpdateDialog::onLinkActivated);
|
||||
mUi->progressBar->reset();
|
||||
adjustDialogSize();
|
||||
}
|
||||
|
||||
void UpdateDialog::setupLoadingUi()
|
||||
{
|
||||
resetUi();
|
||||
mUi->headerContainerLoading->show();
|
||||
mUi->progressBar->show();
|
||||
mUi->progressBar->setMaximum(0);
|
||||
mUi->progressBar->setMinimum(0);
|
||||
mUi->buttonCancelLoading->show();
|
||||
mUi->buttonCancelLoading->setFocus();
|
||||
connect(mUi->buttonCancelLoading, &QPushButton::clicked, this, &UpdateDialog::reject);
|
||||
adjustDialogSize();
|
||||
}
|
||||
|
||||
void UpdateDialog::setupUpdateUi()
|
||||
{
|
||||
resetUi();
|
||||
|
||||
QList<QWidget*> showWidgets;
|
||||
showWidgets << mUi->headerContainer << mUi->labelChangelog << mUi->checkAutoDownload << mUi->buttonCancel << mUi->buttonInstall;
|
||||
for (auto* widget : showWidgets) {
|
||||
widget->show();
|
||||
}
|
||||
|
||||
for (auto* label : {mUi->labelHeadline, mUi->labelInfo}) {
|
||||
QString text = label->text();
|
||||
replaceAppVars(text);
|
||||
label->setText(text);
|
||||
}
|
||||
mUi->labelChangelog->setMarkdown(generateChangelogDocument());
|
||||
|
||||
mUi->checkAutoDownload->setChecked(autoDownloadEnabled(mSettings));
|
||||
|
||||
updateWindowTitle();
|
||||
|
||||
// Show completed progress bar if release has been downloaded already
|
||||
if (mIsDownloadFinished) {
|
||||
mUi->progressBar->show();
|
||||
mUi->progressBar->setMaximum(1);
|
||||
mUi->progressBar->setValue(1);
|
||||
}
|
||||
|
||||
connect(mFeed, &Feed::downloadFinished, this, &UpdateDialog::handleDownloadFinished, Qt::UniqueConnection);
|
||||
connect(mFeed, &Feed::downloadError, this, &UpdateDialog::handleDownloadError, Qt::UniqueConnection);
|
||||
connect(mFeed, &Feed::downloadProgress, this, &UpdateDialog::updateProgressBar, Qt::UniqueConnection);
|
||||
|
||||
connect(mUi->buttonConfirm, &QPushButton::clicked, this, &UpdateDialog::accept);
|
||||
connect(mUi->actionCancel, &QAction::triggered, this, &UpdateDialog::reject);
|
||||
connect(mUi->actionSkip, &QAction::triggered, this, &UpdateDialog::skip);
|
||||
connect(mUi->checkAutoDownload, &QCheckBox::toggled, this, &UpdateDialog::autoDownloadCheckboxToggled);
|
||||
|
||||
if (mInstallButtons.isEmpty()) {
|
||||
mUi->buttonInstall->setFocus();
|
||||
connect(mUi->buttonInstall, &QPushButton::clicked, this, &UpdateDialog::onButtonInstall);
|
||||
} else {
|
||||
mUi->buttonInstall->hide();
|
||||
for (auto* button : mInstallButtons) {
|
||||
button->show();
|
||||
connect(button, &QAbstractButton::clicked, this, &UpdateDialog::onButtonCustomInstall);
|
||||
}
|
||||
mInstallButtons.last()->setFocus();
|
||||
}
|
||||
|
||||
adjustDialogSize();
|
||||
}
|
||||
|
||||
void UpdateDialog::setupChangelogUi()
|
||||
{
|
||||
resetUi();
|
||||
|
||||
QList<QWidget*> showWidgets;
|
||||
showWidgets << mUi->headerContainerChangelog << mUi->buttonConfirm << mUi->labelChangelog;
|
||||
for (auto* widget : showWidgets) {
|
||||
widget->show();
|
||||
}
|
||||
for (auto* label : {mUi->labelHeadlineChangelog, mUi->labelInfoChangelog}) {
|
||||
QString text = label->text();
|
||||
replaceAppVars(text);
|
||||
label->setText(text);
|
||||
}
|
||||
|
||||
updateWindowTitle();
|
||||
|
||||
mUi->labelChangelog->setMarkdown(generateChangelogDocument());
|
||||
connect(mUi->buttonConfirm, &QPushButton::clicked, this, &UpdateDialog::accept);
|
||||
mUi->buttonConfirm->setFocus();
|
||||
adjustDialogSize();
|
||||
}
|
||||
|
||||
void UpdateDialog::setupNoUpdatesUi()
|
||||
{
|
||||
resetUi();
|
||||
QList<QWidget*> showWidgets;
|
||||
showWidgets << mUi->headerContainerNoUpdates << mUi->buttonConfirm;
|
||||
for (auto* widget : showWidgets) {
|
||||
widget->show();
|
||||
}
|
||||
mUi->buttonConfirm->setFocus();
|
||||
|
||||
QString text = mUi->labelHeadlineNoUpdates->text();
|
||||
replaceAppVars(text);
|
||||
mUi->labelHeadlineNoUpdates->setText(text);
|
||||
|
||||
updateWindowTitle();
|
||||
|
||||
connect(mUi->buttonConfirm, &QPushButton::clicked, this, &UpdateDialog::accept);
|
||||
adjustDialogSize();
|
||||
}
|
||||
|
||||
void UpdateDialog::disableButtons(bool disable)
|
||||
{
|
||||
for (auto* button : mInstallButtons) {
|
||||
button->setDisabled(disable);
|
||||
}
|
||||
QList<QWidget*> buttons;
|
||||
buttons << mUi->buttonCancel << mUi->buttonCancelLoading << mUi->buttonConfirm << mUi->buttonInstall << mUi->checkAutoDownload;
|
||||
for (auto* button : buttons) {
|
||||
button->setDisabled(disable);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::replaceAppVars(QString& string)
|
||||
{
|
||||
string.replace("%APPNAME%", QCoreApplication::applicationName());
|
||||
string.replace("%CURRENT_VERSION%", QCoreApplication::applicationVersion());
|
||||
string.replace("%UPDATE_VERSION%", mLatestRelease.getVersion());
|
||||
}
|
||||
|
||||
QString UpdateDialog::generateChangelogDocument()
|
||||
{
|
||||
QString changelog;
|
||||
QList<Release> changelogReleases;
|
||||
if (mMinVersion.isEmpty() && mMaxVersion.isEmpty()) {
|
||||
changelogReleases = mUpdates;
|
||||
} else {
|
||||
Release minRelease(mMinVersion.isEmpty() ? QApplication::applicationVersion() : mMinVersion);
|
||||
Release maxRelease(mMaxVersion);
|
||||
for (const auto& release : mReleases) {
|
||||
if (minRelease < release && (mMaxVersion.isEmpty() || release <= maxRelease)) {
|
||||
changelogReleases << release;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& release : changelogReleases) {
|
||||
changelog.append(qsl("## ") + release.getVersion() + qsl("\n\n"));
|
||||
changelog.append(release.getChangelog() + qsl("\n\n"));
|
||||
}
|
||||
return changelog;
|
||||
}
|
||||
|
||||
void UpdateDialog::startDownload()
|
||||
{
|
||||
mFeed->downloadRelease(mLatestRelease, /*requireChecksums=*/true);
|
||||
disableButtons(true);
|
||||
}
|
||||
|
||||
void UpdateDialog::startUpdate()
|
||||
{
|
||||
if (QDesktopServices::openUrl(QUrl::fromLocalFile(mUpdateFilePath))) {
|
||||
done(QDialog::Accepted);
|
||||
QApplication::quit();
|
||||
} else {
|
||||
qWarning() << "Failed to open update file:" << mUpdateFilePath << "exists:" << QFile::exists(mUpdateFilePath);
|
||||
//: Error shown when the downloaded update file cannot be opened for installation. %1 is the file path.
|
||||
handleDownloadError(tr("Could not open the downloaded update. You can try opening it manually:\n%1").arg(mUpdateFilePath));
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::autoDownloadCheckboxToggled(bool enabled)
|
||||
{
|
||||
enableAutoDownload(enabled, mSettings);
|
||||
}
|
||||
|
||||
void UpdateDialog::handleFeedReady()
|
||||
{
|
||||
mFeedLoadFailed = false;
|
||||
mUpdates = mFeed->getUpdates(dblsqd::Release::getCurrentRelease());
|
||||
mReleases = mFeed->getReleases();
|
||||
if (!mUpdates.isEmpty()) {
|
||||
mLatestRelease = mUpdates.first();
|
||||
}
|
||||
|
||||
if (mType == ManualChangelog) {
|
||||
setupChangelogUi();
|
||||
emit ready();
|
||||
return;
|
||||
}
|
||||
|
||||
mUpdateFilePath = settingsValue(qsl("updateFilePath"), "", mSettings).toString();
|
||||
if (!mUpdateFilePath.isEmpty() && QFile::exists(mUpdateFilePath)) {
|
||||
QString updateFileVersion = settingsValue(qsl("updateFileVersion"), "", mSettings).toString();
|
||||
if (updateFileVersion != mLatestRelease.getVersion() || updateFileVersion == QApplication::applicationVersion()) {
|
||||
if (!QFile::remove(mUpdateFilePath)) {
|
||||
qWarning() << "Failed to remove stale update file:" << mUpdateFilePath;
|
||||
}
|
||||
removeSetting(qsl("updateFilePath"), mSettings);
|
||||
removeSetting(qsl("updateFileVersion"), mSettings);
|
||||
mUpdateFilePath.clear();
|
||||
} else {
|
||||
mIsDownloadFinished = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (mUpdates.isEmpty()) {
|
||||
setupNoUpdatesUi();
|
||||
return;
|
||||
}
|
||||
|
||||
QString latestVersion = mLatestRelease.getVersion();
|
||||
bool skipRelease = (settingsValue(qsl("skipRelease"), "", mSettings).toString() == latestVersion);
|
||||
bool autoDownload = autoDownloadEnabled(mSettings) && (!skipRelease);
|
||||
if (autoDownload && !mIsDownloadFinished) {
|
||||
startDownload();
|
||||
}
|
||||
|
||||
setupUpdateUi();
|
||||
emit ready();
|
||||
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::ready, this, &UpdateDialog::handleFeedReady);
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::loadError, this, &UpdateDialog::handleLoadError);
|
||||
}
|
||||
|
||||
void UpdateDialog::handleLoadError(const QString& message)
|
||||
{
|
||||
qWarning() << "Update check failed:" << message;
|
||||
mFeedLoadFailed = true;
|
||||
if (isVisible()) {
|
||||
setupNoUpdatesUi();
|
||||
//: Label shown in the update dialog when the update check fails due to a network or server error
|
||||
mUi->labelHeadlineNoUpdates->setText(tr("Could not check for updates"));
|
||||
mUi->labelChangelog->setHtml(qsl("<p>%1</p>").arg(message.toHtmlEscaped()));
|
||||
mUi->labelChangelog->show();
|
||||
adjustDialogSize();
|
||||
}
|
||||
// Re-establish single-shot connections so the next feed load attempt reaches this dialog
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::ready, this, &UpdateDialog::handleFeedReady);
|
||||
KDToolBox::connectSingleShot(mFeed, &Feed::loadError, this, &UpdateDialog::handleLoadError);
|
||||
}
|
||||
|
||||
void UpdateDialog::handleDownloadFinished()
|
||||
{
|
||||
const QString filePath = mFeed->getDownloadFilePath();
|
||||
if (filePath.isEmpty()) {
|
||||
//: Error shown when the download finished but no file was saved
|
||||
handleDownloadError(tr("Download failed. Please try again."));
|
||||
return;
|
||||
}
|
||||
mIsDownloadFinished = true;
|
||||
mUpdateFilePath = filePath;
|
||||
setSettingsValue(qsl("updateFilePath"), mUpdateFilePath, mSettings);
|
||||
setSettingsValue(qsl("updateFileVersion"), mLatestRelease.getVersion(), mSettings);
|
||||
|
||||
if (mAccepted) {
|
||||
if (mAcceptedInstallButton == nullptr) {
|
||||
startUpdate();
|
||||
} else {
|
||||
emit installButtonClicked(mAcceptedInstallButton, mUpdateFilePath);
|
||||
}
|
||||
|
||||
} else {
|
||||
disableButtons(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateDialog::handleDownloadError(const QString& message)
|
||||
{
|
||||
//: Title for the download error warning dialog
|
||||
const QString errorTitle = tr("Download Error");
|
||||
//: Message shown in the download error warning dialog, followed by the specific error details
|
||||
QMessageBox::warning(this, errorTitle, tr("There was an error while downloading the update.") + qsl("\n\n") + message);
|
||||
done(QDialog::Rejected);
|
||||
}
|
||||
|
||||
void UpdateDialog::updateProgressBar(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
mUi->progressBar->show();
|
||||
mUi->progressBar->setMaximum(bytesTotal / 1024);
|
||||
mUi->progressBar->setValue(bytesReceived / 1024);
|
||||
}
|
||||
|
||||
void UpdateDialog::onLinkActivated(const QUrl& link)
|
||||
{
|
||||
if (mOpenExternalLinks) {
|
||||
if (!QDesktopServices::openUrl(link)) {
|
||||
qWarning() << "Failed to open URL:" << link;
|
||||
}
|
||||
} else {
|
||||
emit linkActivated(link.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/*! \fn void UpdateDialog::ready()
|
||||
* This signal is emitted when the feed has been loaded and the UpdateDialog is
|
||||
* ready to be shown with show() or exec().
|
||||
* For ManualChangelog type, this is emitted regardless of whether updates are
|
||||
* available.
|
||||
*/
|
||||
|
||||
/*! \fn void UpdateDialog::installButtonClicked(QAbstractButton* button, const QString&
|
||||
* filePath) This signal is emitted when a custom install button was clicked.
|
||||
*/
|
||||
|
||||
} // namespace dblsqd
|
||||
129
src/updater/UpdateDialog.h
Normal file
129
src/updater/UpdateDialog.h
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/***************************************************************************
|
||||
* Copyright (C) 2017 by Philipp Medien - hello@dblsqd.com *
|
||||
* Copyright (C) 2026 by Vadim Peretokin - vperetokin@gmail.com *
|
||||
* *
|
||||
* This program is free software; you can redistribute it and/or modify *
|
||||
* it under the terms of the GNU General Public License as published by *
|
||||
* the Free Software Foundation; either version 2 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* This program is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU General Public License *
|
||||
* along with this program; if not, write to the *
|
||||
* Free Software Foundation, Inc., *
|
||||
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
|
||||
***************************************************************************/
|
||||
|
||||
#ifndef DBLSQD_UPDATE_DIALOG_H
|
||||
#define DBLSQD_UPDATE_DIALOG_H
|
||||
|
||||
#include "Release.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QVariant>
|
||||
|
||||
class QAbstractButton;
|
||||
class QPixmap;
|
||||
class QSettings;
|
||||
|
||||
namespace Ui {
|
||||
class UpdateDialog;
|
||||
}
|
||||
|
||||
namespace dblsqd {
|
||||
|
||||
class Feed;
|
||||
|
||||
class UpdateDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Type { OnUpdateAvailable, OnLastWindowClosed, Manual, ManualChangelog };
|
||||
explicit UpdateDialog(Feed* feed, Type type, QSettings* settings, QWidget* parent = nullptr);
|
||||
~UpdateDialog();
|
||||
|
||||
void setIcon(const QString& fileName);
|
||||
void setIcon(const QPixmap& pixmap);
|
||||
void addInstallButton(QAbstractButton* button);
|
||||
|
||||
void setMinVersion(const QString& version);
|
||||
void setMaxVersion(const QString& version);
|
||||
void setPreviousVersion(const QString& version);
|
||||
|
||||
static bool autoDownloadEnabled(QVariant defaultValue, QSettings* settings);
|
||||
static bool autoDownloadEnabled(QSettings* settings);
|
||||
static void enableAutoDownload(bool enabled, QSettings* settings);
|
||||
|
||||
void setOpenExternalLinks(bool open);
|
||||
bool openExternalLinks() const;
|
||||
|
||||
signals:
|
||||
void ready();
|
||||
void installButtonClicked(QAbstractButton* button, const QString& filePath);
|
||||
void linkActivated(const QString& link);
|
||||
|
||||
public slots:
|
||||
void onButtonInstall();
|
||||
void onButtonCustomInstall();
|
||||
void skip();
|
||||
void showIfUpdatesAvailable();
|
||||
void showIfUpdatesAvailableOrQuit();
|
||||
|
||||
private:
|
||||
Ui::UpdateDialog* mUi;
|
||||
Feed* mFeed;
|
||||
Type mType;
|
||||
|
||||
QSettings* mSettings;
|
||||
void replaceAppVars(QString& string);
|
||||
QString generateChangelogDocument();
|
||||
|
||||
void disableButtons(bool disable = true);
|
||||
void resetUi();
|
||||
void setupLoadingUi();
|
||||
void setupUpdateUi();
|
||||
void setupChangelogUi();
|
||||
void setupNoUpdatesUi();
|
||||
void adjustDialogSize();
|
||||
void updateWindowTitle();
|
||||
|
||||
void startDownload();
|
||||
void startUpdate();
|
||||
|
||||
bool mAccepted{false};
|
||||
bool mIsDownloadFinished{false};
|
||||
bool mFeedLoadFailed{false};
|
||||
QString mUpdateFilePath;
|
||||
QList<Release> mReleases;
|
||||
QList<Release> mUpdates;
|
||||
Release mLatestRelease;
|
||||
QList<QAbstractButton*> mInstallButtons;
|
||||
QAbstractButton* mAcceptedInstallButton;
|
||||
bool mOpenExternalLinks{true};
|
||||
QString mMinVersion;
|
||||
QString mMaxVersion;
|
||||
QString mPreviousVersion;
|
||||
|
||||
static void setSettingsValue(const QString& key, const QVariant& value, QSettings* settings);
|
||||
static QVariant settingsValue(const QString& key, const QVariant& defaultValue, QSettings* settings);
|
||||
static void removeSetting(const QString& key, QSettings* settings);
|
||||
static void setDefaultSettingsValue(const QString& key, const QVariant& value, QSettings* settings);
|
||||
|
||||
private slots:
|
||||
void handleFeedReady();
|
||||
void handleLoadError(const QString& message);
|
||||
void handleDownloadFinished();
|
||||
void handleDownloadError(const QString& message);
|
||||
void updateProgressBar(qint64, qint64);
|
||||
void autoDownloadCheckboxToggled(bool enabled = true);
|
||||
void onLinkActivated(const QUrl& link);
|
||||
};
|
||||
|
||||
} // namespace dblsqd
|
||||
|
||||
#endif // DBLSQD_UPDATE_DIALOG_H
|
||||
372
src/updater/update_dialog.ui
Normal file
372
src/updater/update_dialog.ui
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>UpdateDialog</class>
|
||||
<widget class="QDialog" name="UpdateDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>645</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>%APPNAME% update</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QWidget" name="headerContainerLoading" native="true">
|
||||
<layout class="QGridLayout" name="gridLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelHeadlineLoading">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Loading update information …</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="headerContainer" native="true">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>18</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="labelHeadline">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>A new version of %APPNAME% is available!</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0" rowspan="3">
|
||||
<widget class="QLabel" name="labelIcon">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2" rowspan="2">
|
||||
<widget class="QLabel" name="labelInfo">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>%APPNAME% %UPDATE_VERSION% is available (you have %CURRENT_VERSION%).
|
||||
Would you like to update now?</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="headerContainerChangelog" native="true">
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>18</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelHeadlineChangelog">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Changelog for %APPNAME%</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="labelInfoChangelog">
|
||||
<property name="text">
|
||||
<string>You are using version %CURRENT_VERSION%.</string>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="headerContainerNoUpdates" native="true">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>18</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>24</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="labelInfoNoUpdates">
|
||||
<property name="text">
|
||||
<string>There are currently no updates available.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="labelHeadlineNoUpdates">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>You are using %APPNAME% %CURRENT_VERSION%.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="labelChangelog">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>150</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::StrongFocus</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>24</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkAutoDownload">
|
||||
<property name="text">
|
||||
<string>Automatically download future updates</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="buttonContainer" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>12</number>
|
||||
</property>
|
||||
<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="QToolButton" name="buttonCancel">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::ActionsContextMenu</enum>
|
||||
</property>
|
||||
<property name="popupMode">
|
||||
<enum>QToolButton::MenuButtonPopup</enum>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonFollowStyle</enum>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonCancelLoading">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonInstall">
|
||||
<property name="text">
|
||||
<string>Install update now</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonConfirm">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<action name="actionCancel">
|
||||
<property name="text">
|
||||
<string>Remind me later</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSkip">
|
||||
<property name="text">
|
||||
<string>Skip this version</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<layoutdefault spacing="6" margin="11"/>
|
||||
<tabstops>
|
||||
<tabstop>labelChangelog</tabstop>
|
||||
<tabstop>checkAutoDownload</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Loading…
Add table
Add a link
Reference in a new issue