diff --git a/.gitignore b/.gitignore index 503adc2d..e2284dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ deploy/build_32/* deploy/build_64/* winbuild*.bat .cache/ +.vscode/ # Qt-es diff --git a/client/client_scripts/linux_installer.sh b/client/client_scripts/linux_installer.sh new file mode 100644 index 00000000..82987535 --- /dev/null +++ b/client/client_scripts/linux_installer.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +EXTRACT_DIR="$1" +INSTALLER_PATH="$2" + +# Create and clean extract directory +rm -rf "$EXTRACT_DIR" +mkdir -p "$EXTRACT_DIR" + +# Extract ZIP archive +unzip "$INSTALLER_PATH" -d "$EXTRACT_DIR" +if [ $? -ne 0 ]; then + echo 'Failed to extract ZIP archive' + exit 1 +fi + +# Find and extract TAR archive +TAR_FILE=$(find "$EXTRACT_DIR" -name '*.tar' -type f) +if [ -z "$TAR_FILE" ]; then + echo 'TAR file not found' + exit 1 +fi + +tar -xf "$TAR_FILE" -C "$EXTRACT_DIR" +if [ $? -ne 0 ]; then + echo 'Failed to extract TAR archive' + exit 1 +fi + +rm -f "$TAR_FILE" + +# Find and run installer +INSTALLER=$(find "$EXTRACT_DIR" -type f -executable) +if [ -z "$INSTALLER" ]; then + echo 'Installer not found' + exit 1 +fi + +"$INSTALLER" +EXIT_CODE=$? + +# Cleanup +rm -rf "$EXTRACT_DIR" +exit $EXIT_CODE \ No newline at end of file diff --git a/client/client_scripts/mac_installer.sh b/client/client_scripts/mac_installer.sh new file mode 100644 index 00000000..186f1502 --- /dev/null +++ b/client/client_scripts/mac_installer.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +EXTRACT_DIR="$1" +INSTALLER_PATH="$2" + +# Create and clean extract directory +rm -rf "$EXTRACT_DIR" +mkdir -p "$EXTRACT_DIR" + +# Mount the DMG +MOUNT_POINT="$EXTRACT_DIR/mounted_dmg" +hdiutil attach "$INSTALLER_PATH" -mountpoint "$MOUNT_POINT" +if [ $? -ne 0 ]; then + echo "Failed to mount DMG" + exit 1 +fi + +# Check if the application exists in the mounted DMG +if [ ! -d "$MOUNT_POINT/AmneziaVPN.app" ]; then + echo "Error: AmneziaVPN.app not found in the mounted DMG." + hdiutil detach "$MOUNT_POINT" #-quiet + exit 1 +fi + +# Run the application +echo "Running AmneziaVPN.app from the mounted DMG..." +open "$MOUNT_POINT/AmneziaVPN.app" + +# Get the PID of the app launched from the DMG +APP_PATH="$MOUNT_POINT/AmneziaVPN.app" +PID=$(pgrep -f "$APP_PATH") + +if [ -z "$PID" ]; then + echo "Failed to retrieve PID for AmneziaVPN.app" + hdiutil detach "$MOUNT_POINT" + exit 1 +fi + +# Wait for the specific PID to exit +echo "Waiting for AmneziaVPN.app to exit..." +while kill -0 "$PID" 2>/dev/null; do + sleep 1 +done + +# Unmount the DMG +hdiutil detach "$EXTRACT_DIR/mounted_dmg" +if [ $? -ne 0 ]; then + echo "Failed to unmount DMG" + exit 1 +fi + +# Optional: Remove the DMG file +rm "$INSTALLER_PATH" + +echo "Installation completed successfully" +exit 0 diff --git a/client/core/controllers/coreController.cpp b/client/core/controllers/coreController.cpp index 0e72ef1a..193aa9fe 100644 --- a/client/core/controllers/coreController.cpp +++ b/client/core/controllers/coreController.cpp @@ -151,6 +151,9 @@ void CoreController::initControllers() m_apiPremV1MigrationController.reset(new ApiPremV1MigrationController(m_serversModel, m_settings, this)); m_engine->rootContext()->setContextProperty("ApiPremV1MigrationController", m_apiPremV1MigrationController.get()); + + m_updateController.reset(new UpdateController(m_settings)); + m_engine->rootContext()->setContextProperty("UpdateController", m_updateController.get()); } void CoreController::initAndroidController() @@ -226,6 +229,7 @@ void CoreController::initSignalHandlers() initImportPremiumV2VpnKeyHandler(); initShowMigrationDrawerHandler(); initStrictKillSwitchHandler(); + initUpdateFoundHandler(); } void CoreController::initNotificationHandler() @@ -393,6 +397,16 @@ void CoreController::initStrictKillSwitchHandler() &VpnConnection::onKillSwitchModeChanged); } +void CoreController::initUpdateFoundHandler() +{ +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + connect(m_updateController.get(), &UpdateController::updateFound, this, + [this]() { QTimer::singleShot(1000, this, [this]() { m_pageController->showChangelogDrawer(); }); }); + + m_updateController->checkForUpdates(); +#endif +} + QSharedPointer CoreController::pageController() const { return m_pageController; diff --git a/client/core/controllers/coreController.h b/client/core/controllers/coreController.h index 9ae53562..c81e75e2 100644 --- a/client/core/controllers/coreController.h +++ b/client/core/controllers/coreController.h @@ -19,6 +19,7 @@ #include "ui/controllers/settingsController.h" #include "ui/controllers/sitesController.h" #include "ui/controllers/systemController.h" +#include "ui/controllers/updateController.h" #include "ui/models/allowed_dns_model.h" #include "ui/models/containers_model.h" @@ -86,6 +87,7 @@ private: void initImportPremiumV2VpnKeyHandler(); void initShowMigrationDrawerHandler(); void initStrictKillSwitchHandler(); + void initUpdateFoundHandler(); QQmlApplicationEngine *m_engine {}; // TODO use parent child system here? std::shared_ptr m_settings; @@ -109,6 +111,7 @@ private: QScopedPointer m_systemController; QScopedPointer m_appSplitTunnelingController; QScopedPointer m_allowedDnsController; + QScopedPointer m_updateController; QScopedPointer m_apiSettingsController; QScopedPointer m_apiConfigsController; diff --git a/client/core/scripts_registry.cpp b/client/core/scripts_registry.cpp index 95b5df4a..d2b17cb9 100644 --- a/client/core/scripts_registry.cpp +++ b/client/core/scripts_registry.cpp @@ -54,6 +54,15 @@ QString amnezia::scriptName(ProtocolScriptType type) } } +QString amnezia::scriptName(ClientScriptType type) +{ + switch (type) { + case ClientScriptType::linux_installer: return QLatin1String("linux_installer.sh"); + case ClientScriptType::mac_installer: return QLatin1String("mac_installer.sh"); + default: return QString(); + } +} + QString amnezia::scriptData(amnezia::SharedScriptType type) { QString fileName = QString(":/server_scripts/%1").arg(amnezia::scriptName(type)); @@ -81,3 +90,19 @@ QString amnezia::scriptData(amnezia::ProtocolScriptType type, DockerContainer co data.replace("\r", ""); return data; } + +QString amnezia::scriptData(ClientScriptType type) +{ + QString fileName = QString(":/client_scripts/%1").arg(amnezia::scriptName(type)); + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Warning: script missing" << fileName; + return ""; + } + QByteArray data = file.readAll(); + if (data.isEmpty()) { + qDebug() << "Warning: script is empty" << fileName; + } + data.replace("\r", ""); + return data; +} diff --git a/client/core/scripts_registry.h b/client/core/scripts_registry.h index d952dafb..87fddbb5 100644 --- a/client/core/scripts_registry.h +++ b/client/core/scripts_registry.h @@ -1,44 +1,53 @@ #ifndef SCRIPTS_REGISTRY_H #define SCRIPTS_REGISTRY_H -#include -#include "core/defs.h" #include "containers/containers_defs.h" +#include "core/defs.h" +#include -namespace amnezia { +namespace amnezia +{ -enum SharedScriptType { - // General scripts - prepare_host, - install_docker, - build_container, - remove_container, - remove_all_containers, - setup_host_firewall, - check_connection, - check_server_is_busy, - check_user_in_sudo -}; -enum ProtocolScriptType { - // Protocol scripts - dockerfile, - run_container, - configure_container, - container_startup, - openvpn_template, - wireguard_template, - awg_template, - xray_template -}; + enum SharedScriptType { + // General scripts + prepare_host, + install_docker, + build_container, + remove_container, + remove_all_containers, + setup_host_firewall, + check_connection, + check_server_is_busy, + check_user_in_sudo + }; + enum ProtocolScriptType { + // Protocol scripts + dockerfile, + run_container, + configure_container, + container_startup, + openvpn_template, + wireguard_template, + awg_template, + xray_template + }; -QString scriptFolder(DockerContainer container); + enum ClientScriptType { + // Client-side scripts + linux_installer, + mac_installer + }; -QString scriptName(SharedScriptType type); -QString scriptName(ProtocolScriptType type); + QString scriptFolder(DockerContainer container); -QString scriptData(SharedScriptType type); -QString scriptData(ProtocolScriptType type, DockerContainer container); + QString scriptName(SharedScriptType type); + QString scriptName(ProtocolScriptType type); + QString scriptName(ClientScriptType type); + + QString scriptData(SharedScriptType type); + QString scriptData(ProtocolScriptType type, DockerContainer container); + QString scriptData(ClientScriptType type); } #endif // SCRIPTS_REGISTRY_H diff --git a/client/resources.qrc b/client/resources.qrc index 72eb15c7..c88a0867 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -59,6 +59,9 @@ images/tray/active.png images/tray/default.png images/tray/error.png + client_scripts/linux_installer.sh + client_scripts/mac_installer.sh + server_scripts/openvpn_cloak/Dockerfile server_scripts/awg/configure_container.sh server_scripts/awg/Dockerfile server_scripts/awg/run_container.sh @@ -175,10 +178,11 @@ ui/qml/Controls2/TopCloseButtonType.qml ui/qml/Controls2/VerticalRadioButton.qml ui/qml/Controls2/WarningType.qml + ui/qml/Components/ChangelogDrawer.qml + ui/qml/Modules/Style/qmldir ui/qml/Filters/ContainersModelFilters.qml ui/qml/main2.qml ui/qml/Modules/Style/AmneziaStyle.qml - ui/qml/Modules/Style/qmldir ui/qml/Pages2/PageDeinstalling.qml ui/qml/Pages2/PageDevMenu.qml ui/qml/Pages2/PageHome.qml diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index fc981091..a9f4fcc2 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -145,6 +145,7 @@ signals: void escapePressed(); void closeTopDrawer(); + void showChangelogDrawer(); private: QSharedPointer m_serversModel; diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 8cb3a0d1..1f4fbed3 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -9,7 +9,7 @@ class SystemController : public QObject { Q_OBJECT public: - explicit SystemController(const std::shared_ptr &setting, QObject *parent = nullptr); + explicit SystemController(const std::shared_ptr &settings, QObject *parent = nullptr); static void saveFile(const QString &fileName, const QString &data); static bool readFile(const QString &fileName, QByteArray &data); diff --git a/client/ui/controllers/updateController.cpp b/client/ui/controllers/updateController.cpp new file mode 100644 index 00000000..c98d7ccc --- /dev/null +++ b/client/ui/controllers/updateController.cpp @@ -0,0 +1,385 @@ +#include "updateController.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "amnezia_application.h" +#include "core/errorstrings.h" +#include "core/scripts_registry.h" +#include "logger.h" +#include "version.h" +#include "core/controllers/gatewayController.h" + +namespace +{ + Logger logger("UpdateController"); + +#ifdef Q_OS_MACOS + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.dmg"; +#elif defined Q_OS_WINDOWS + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN_installer.exe"; +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + const QString installerPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/AmneziaVPN.tar.zip"; +#endif +} + +UpdateController::UpdateController(const std::shared_ptr &settings, QObject *parent) : QObject(parent), m_settings(settings) +{ +} + +QString UpdateController::getHeaderText() +{ + return tr("New version released: %1 (%2)").arg(m_version, m_releaseDate); +} + +QString UpdateController::getChangelogText() +{ + QStringList lines = m_changelogText.split("\n"); + QStringList filteredChangeLogText; + bool add = false; + QString osSection; + +#ifdef Q_OS_WINDOWS + osSection = "### Windows"; +#elif defined(Q_OS_MACOS) + osSection = "### macOS"; +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + osSection = "### Linux"; +#endif + + for (const QString &line : lines) { + if (line.startsWith("### General")) { + add = true; + } else if (line.startsWith("### ") && line != osSection) { + add = false; + } else if (line == osSection) { + add = true; + } + + if (add) { + filteredChangeLogText.append(line); + } + } + + return filteredChangeLogText.join("\n"); +} + +void UpdateController::checkForUpdates() +{ + qDebug() << "checkForUpdates"; + if (!fetchGatewayUrl()) return; + if (!fetchVersionInfo()) return; + if (!isNewVersionAvailable()) return; + if (!fetchChangelog()) return; + if (!fetchReleaseDate()) return; + + m_downloadUrl = composeDownloadUrl(); + emit updateFound(); +} + +bool UpdateController::fetchGatewayUrl() +{ + GatewayController gatewayController(m_settings->getGatewayEndpoint(), + m_settings->isDevGatewayEnv(), + 7000, + m_settings->isStrictKillSwitchEnabled()); + + QByteArray gatewayResponse; + auto err = gatewayController.get(QStringLiteral("%1v1/updater_endpoint"), gatewayResponse); + if (err != ErrorCode::NoError) { + logger.error() << errorString(err); + return false; + } + + QJsonObject gatewayData = QJsonDocument::fromJson(gatewayResponse).object(); + qDebug() << "gatewayData:" << gatewayData; + + QString baseUrl = gatewayData.value("url").toString(); + if (baseUrl.endsWith('/')) { + baseUrl.chop(1); + } + + m_baseUrl = baseUrl; + return true; +} + +bool UpdateController::fetchVersionInfo() +{ + QByteArray data; + if (!doSyncGet("/VERSION", data)) { + return false; + } + m_version = QString::fromUtf8(data).trimmed(); + return true; +} + +bool UpdateController::isNewVersionAvailable() +{ + auto currentVersion = QVersionNumber::fromString(QString(APP_VERSION)); + auto newVersion = QVersionNumber::fromString(m_version); + return newVersion > currentVersion; +} + +bool UpdateController::fetchChangelog() +{ + QByteArray data; + if (!doSyncGet("/CHANGELOG", data)) { + m_changelogText = tr("Failed to load changelog text"); + } else { + m_changelogText = QString::fromUtf8(data); + } + return true; +} + +bool UpdateController::fetchReleaseDate() +{ + QByteArray data; + if (!doSyncGet("/RELEASE_DATE", data)) { + m_releaseDate = QStringLiteral("Failed to load release date"); + } else { + m_releaseDate = QString::fromUtf8(data).trimmed(); + } + return true; +} + +void UpdateController::setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation) +{ + QObject::connect(reply, &QNetworkReply::errorOccurred, [this, reply, operation](QNetworkReply::NetworkError error) { + logger.error() << QString("Network error occurred while fetching %1: %2 %3") + .arg(operation, reply->errorString(), QString::number(error)); + }); + + QObject::connect(reply, &QNetworkReply::sslErrors, [this, reply, operation](const QList &errors) { + QStringList errorStrings; + for (const QSslError &err : errors) { + errorStrings << err.errorString(); + } + logger.error() << QString("SSL errors while fetching %1: %2").arg(operation, errorStrings.join("; ")); + }); +} + +void UpdateController::handleNetworkError(QNetworkReply* reply, const QString& operation) +{ + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = reply->errorString(); + logger.error() << "Network error code:" << QString::number(static_cast(reply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); + } +} + +QString UpdateController::composeDownloadUrl() +{ + QString fileName; +#if defined(Q_OS_WINDOWS) + fileName = QString("AmneziaVPN_%1_x64.exe").arg(m_version); +#elif defined(Q_OS_MACOS) + fileName = QString("AmneziaVPN_%1_macos.dmg").arg(m_version); +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + fileName = QString("AmneziaVPN_%1_linux.tar.zip").arg(m_version); +#endif + return m_baseUrl + "/" + fileName; +} + +void UpdateController::runInstaller() +{ +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) + if (m_downloadUrl.isEmpty()) { + logger.error() << "Download URL is empty"; + return; + } + + QNetworkRequest request; + request.setTransferTimeout(7000); + request.setUrl(m_downloadUrl); + + QNetworkReply *reply = amnApp->networkManager()->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [this, reply]() { + if (reply->error() == QNetworkReply::NoError) { + QFile file(installerPath); + if (!file.open(QIODevice::WriteOnly)) { + logger.error() << "Failed to open installer file for writing:" << installerPath << "Error:" << file.errorString(); + reply->deleteLater(); + return; + } + + if (file.write(reply->readAll()) == -1) { + logger.error() << "Failed to write installer data to file:" << installerPath << "Error:" << file.errorString(); + file.close(); + reply->deleteLater(); + return; + } + + file.close(); + + #if defined(Q_OS_WINDOWS) + runWindowsInstaller(installerPath); + #elif defined(Q_OS_MACOS) + runMacInstaller(installerPath); + #elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + runLinuxInstaller(installerPath); + #endif + } else { + if (reply->error() == QNetworkReply::NetworkError::OperationCanceledError + || reply->error() == QNetworkReply::NetworkError::TimeoutError) { + logger.error() << errorString(ErrorCode::ApiConfigTimeoutError); + } else { + QString err = reply->errorString(); + logger.error() << QString::fromUtf8(reply->readAll()); + logger.error() << "Network error code:" << QString::number(static_cast(reply->error())); + logger.error() << "Error message:" << err; + logger.error() << "HTTP status:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + logger.error() << errorString(ErrorCode::ApiConfigDownloadError); + } + } + reply->deleteLater(); + }); +#endif +} + +#if defined(Q_OS_WINDOWS) +int UpdateController::runWindowsInstaller(const QString &installerPath) +{ + qint64 pid; + bool success = QProcess::startDetached(installerPath, QStringList(), QString(), &pid); + + if (success) { + logger.info() << "Installation process started with PID:" << pid; + } else { + logger.error() << "Failed to start installation process"; + return -1; + } + + return 0; +} +#endif + +#if defined(Q_OS_MACOS) +int UpdateController::runMacInstaller(const QString &installerPath) +{ + // Create temporary directory for extraction + QTemporaryDir extractDir; + extractDir.setAutoRemove(false); + if (!extractDir.isValid()) { + logger.error() << "Failed to create temporary directory"; + return -1; + } + logger.info() << "Temporary directory created:" << extractDir.path(); + + // Create script file in the temporary directory + QString scriptPath = extractDir.path() + "/mac_installer.sh"; + QFile scriptFile(scriptPath); + if (!scriptFile.open(QIODevice::WriteOnly)) { + logger.error() << "Failed to create script file"; + return -1; + } + + // Get script content from registry + QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::mac_installer); + if (scriptContent.isEmpty()) { + logger.error() << "macOS installer script content is empty"; + scriptFile.close(); + return -1; + } + + scriptFile.write(scriptContent.toUtf8()); + scriptFile.close(); + logger.info() << "Script file created:" << scriptPath; + + // Make script executable + QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); + + // Start detached process + qint64 pid; + bool success = + QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); + + if (success) { + logger.info() << "Installation process started with PID:" << pid; + } else { + logger.error() << "Failed to start installation process"; + return -1; + } + + return 0; +} +#endif + +#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) +int UpdateController::runLinuxInstaller(const QString &installerPath) +{ + // Create temporary directory for extraction + QTemporaryDir extractDir; + extractDir.setAutoRemove(false); + if (!extractDir.isValid()) { + logger.error() << "Failed to create temporary directory"; + return -1; + } + logger.info() << "Temporary directory created:" << extractDir.path(); + + // Create script file in the temporary directory + QString scriptPath = extractDir.path() + "/installer.sh"; + QFile scriptFile(scriptPath); + if (!scriptFile.open(QIODevice::WriteOnly)) { + logger.error() << "Failed to create script file"; + return -1; + } + + // Get script content from registry + QString scriptContent = amnezia::scriptData(amnezia::ClientScriptType::linux_installer); + scriptFile.write(scriptContent.toUtf8()); + scriptFile.close(); + logger.info() << "Script file created:" << scriptPath; + + // Make script executable + QFile::setPermissions(scriptPath, QFile::permissions(scriptPath) | QFile::ExeUser); + + // Start detached process + qint64 pid; + bool success = + QProcess::startDetached("/bin/bash", QStringList() << scriptPath << extractDir.path() << installerPath, extractDir.path(), &pid); + + if (success) { + logger.info() << "Installation process started with PID:" << pid; + } else { + logger.error() << "Failed to start installation process"; + return -1; + } + + return 0; +} +#endif + +bool UpdateController::doSyncGet(const QString& endpoint, QByteArray& outData) +{ + QNetworkRequest req; + req.setTransferTimeout(7000); + req.setUrl(QUrl(m_baseUrl + endpoint)); + + QNetworkReply* reply = amnApp->networkManager()->get(req); + setupNetworkErrorHandling(reply, endpoint); + + QEventLoop loop; + QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + + bool ok = (reply->error() == QNetworkReply::NoError); + if (ok) { + outData = reply->readAll(); + } else { + handleNetworkError(reply, endpoint); + } + reply->deleteLater(); + return ok; +} diff --git a/client/ui/controllers/updateController.h b/client/ui/controllers/updateController.h new file mode 100644 index 00000000..4dfcf5d8 --- /dev/null +++ b/client/ui/controllers/updateController.h @@ -0,0 +1,54 @@ +#ifndef UPDATECONTROLLER_H +#define UPDATECONTROLLER_H + +#include +#include + +#include "settings.h" + +class UpdateController : public QObject +{ + Q_OBJECT +public: + explicit UpdateController(const std::shared_ptr &settings, QObject *parent = nullptr); + + Q_PROPERTY(QString changelogText READ getChangelogText NOTIFY updateFound) + Q_PROPERTY(QString headerText READ getHeaderText NOTIFY updateFound) +public slots: + QString getHeaderText(); + QString getChangelogText(); + + void checkForUpdates(); + void runInstaller(); +signals: + void updateFound(); + +private: + bool fetchGatewayUrl(); + bool fetchVersionInfo(); + bool fetchChangelog(); + bool fetchReleaseDate(); + bool isNewVersionAvailable(); + bool doSyncGet(const QString& endpoint, QByteArray& outData); + void setupNetworkErrorHandling(QNetworkReply* reply, const QString& operation); + void handleNetworkError(QNetworkReply* reply, const QString& operation); + QString composeDownloadUrl(); + + std::shared_ptr m_settings; + + QString m_baseUrl; + QString m_changelogText; + QString m_version; + QString m_releaseDate; + QString m_downloadUrl; + +#if defined(Q_OS_WINDOWS) + int runWindowsInstaller(const QString &installerPath); +#elif defined(Q_OS_MACOS) + int runMacInstaller(const QString &installerPath); +#elif defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) + int runLinuxInstaller(const QString &installerPath); +#endif +}; + +#endif // UPDATECONTROLLER_H diff --git a/client/ui/qml/Components/ChangelogDrawer.qml b/client/ui/qml/Components/ChangelogDrawer.qml new file mode 100644 index 00000000..1bb767be --- /dev/null +++ b/client/ui/qml/Components/ChangelogDrawer.qml @@ -0,0 +1,103 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +DrawerType2 { + id: root + + anchors.fill: parent + expandedHeight: parent.height * 0.9 + + expandedStateContent: Item { + implicitHeight: root.expandedHeight + + Header2TextType { + id: header + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.bottomMargin: 16 + + text: UpdateController.headerText + } + + FlickableType { + anchors.top: header.bottom + anchors.bottom: updateButton.top + contentHeight: changelog.height + 32 + + ParagraphTextType { + id: changelog + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + anchors.bottomMargin: 16 + + HoverHandler { + enabled: parent.hoveredLink + cursorShape: Qt.PointingHandCursor + } + + onLinkActivated: function(link) { + Qt.openUrlExternally(link) + } + + text: UpdateController.changelogText + textFormat: Text.MarkdownText + } + } + + BasicButtonType { + id: updateButton + anchors.bottom: skipButton.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 8 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + text: qsTr("Update") + + clickedFunc: function() { + PageController.showBusyIndicator(true) + UpdateController.runInstaller() + PageController.showBusyIndicator(false) + root.closeTriggered() + } + } + + BasicButtonType { + id: skipButton + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottomMargin: 16 + anchors.rightMargin: 16 + anchors.leftMargin: 16 + + defaultColor: "transparent" + hoveredColor: Qt.rgba(1, 1, 1, 0.08) + pressedColor: Qt.rgba(1, 1, 1, 0.12) + disabledColor: "#878B91" + textColor: "#D7D8DB" + borderWidth: 1 + + text: qsTr("Skip") + + clickedFunc: function() { + root.closeTriggered() + } + } + } +} diff --git a/client/ui/qml/main2.qml b/client/ui/qml/main2.qml index 7cd5790b..4a14a5c3 100644 --- a/client/ui/qml/main2.qml +++ b/client/ui/qml/main2.qml @@ -96,6 +96,10 @@ Window { busyIndicator.visible = visible PageController.disableControls(visible) } + + function onShowChangelogDrawer() { + changelogDrawer.openTriggered() + } } Connections { @@ -285,4 +289,14 @@ Window { onAccepted: SystemController.fileDialogClosed(true) onRejected: SystemController.fileDialogClosed(false) } + + Item { + anchors.fill: parent + + ChangelogDrawer { + id: changelogDrawer + + anchors.fill: parent + } + } } diff --git a/deploy/deploy_s3.sh b/deploy/deploy_s3.sh index a139a5a5..b8aa8821 100755 --- a/deploy/deploy_s3.sh +++ b/deploy/deploy_s3.sh @@ -12,7 +12,8 @@ mkdir -p dist cd dist -echo $VERSION >> VERSION +echo $VERSION > VERSION +curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .published_at > RELEASE_DATE curl -s https://api.github.com/repos/amnezia-vpn/amnezia-client/releases/tags/$VERSION | jq -r .body | tr -d '\r' > CHANGELOG if [[ $(cat CHANGELOG) = null ]]; then