Files
amnezia-client/client/ui/qml/main2.qml
Nethius c28452a5da feat: desktop updater (#825)
* added changelog drawer

* Created a scaffold for Linux installation

* implement Linux updating

* Add debug logs about installer in service

* Add client side of installation logic for Windows and MacOS

* Add service side of installation logic for Windows

* ru readme

* Update README_RU.md

* Add files via upload

* chore: added clang-format config files (#1293)

* Update README_RU.md

* Update README.md

* feature: added subscription expiration date for premium v2 (#1261)

* feature: added subscription expiration date for premium v2

* feature: added a check for the presence of the “services” field in the response body of the getServicesList() function

* feature: added prohibition to change location when connection is active

* bugfix: renamed public_key->end_date to public_key->expires_at according to the changes on the backend

* feature/xray user management (#972)

* feature: implement client management functionality for Xray

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: vladimir.kuznetsov <nethiuswork@gmail.com>

* Fix formatting

* Add some logs

* Add logs from installattion shell on Windows

* Fix installation for Windows and MacOS

* Optimized code

* Move installer running to client side for Ubuntu

* Move installer launch logic to client side for Windows

* Clean service code

* Add linux_install script to resources

* Add logs for UpdateController

* Add draft for MacOS installation

* Disable updates checking for Android and iOS

* chore: fixed macos update script

* chore: remove duplicate lines

* chore: post merge fixes

* chore: add missing ifdef

* decrease version for testing

* chore: added changelog text processing depend on OS

* add .vscode to .gitignore

* Change updater downloading method to retrieving link from the gateway

* add Release date file creation to s3 deploy script

* Add release date downloading from endpoint

* update check refactoring

* feat: switch macOS auto-update from DMG to ZIP+PKG installer

- Update macOS artifact URL from .dmg to .zip
- Rewrite mac_installer.sh to extract ZIP and install PKG via osascript
- Increase download timeout to 30s for larger ZIP files

* fix: fix Android build

* feat: Change get request for updater link to post

* refactor: preparing NewsModel for update notifications

- Changed `updateModel` to `setNewsList` for better semantic meaning.
- Delegate model container updating to private method updateModel
- Updated the logic for marking news as read to use item IDs instead of a boolean flag.

* feat: Move update notification in news list

- Updated `UpdateController` to handle empty release dates in header text.
- Added `getVersion` method to `UpdateController` for version retrieval.
- Enhanced `NewsModel` to support update notifications with new methods for marking updates as skipped and setting update notifications.
- Updated QML pages to display update information and provide actions for updates and skipping them.
- Introduced `isUpdate` property in `NewsItem` to differentiate between regular news and updates.

* feat: Implement rate limit workaround for gateway requests

- Added a delay before contacting the gateway in both `UpdateController` and `ApiNewsController` to prevent rate limit issues caused by simultaneous requests.

* refactor: Convert synchronous network requests to asynchronous in UpdateController

- Updated `UpdateController` to use asynchronous network requests for fetching gateway URL, version info, changelog, and release date.
- Introduced `doGetAsync` method to handle asynchronous GET requests with error handling.
- Removed synchronous methods to improve responsiveness and prevent blocking the UI during network operations.
- Added a mechanism to prevent multiple concurrent update checks.

* chore: Decrease AmneziaVPN version to 4.8.10.0 in CMakeLists.txt for testing

* refactor: Improve update check handling to avoid rate limit issues

- Updated `CoreController` to initiate update checks after news fetching is complete.
- Removed synchronous waiting in `ApiNewsController` to streamline the fetching process.

* fix: fixed typo in IsReadRole

* fix: fix updater filenames

* chore: move updateController to core

* refactor: update to mvvm

* chore: tiny fix

---------

Co-authored-by: aiamnezia <ai@amnezia.org>
Co-authored-by: aiamnezia <ai@amnezia.com>
Co-authored-by: Pokamest Nikak <pokamest@gmail.com>
Co-authored-by: KsZnak <ksu@amnezia.org>
Co-authored-by: Cyril Anisimov <cyan84@gmail.com>
Co-authored-by: vkamn <vk@amnezia.org>
2026-05-04 12:37:19 +08:00

378 lines
10 KiB
QML

import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import PageEnum 1.0
import Style 1.0
import "Config"
import "Controls2"
import "Components"
import "Pages2"
Window {
id: root
objectName: "mainWindow"
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.platform.os === "android") {
if (Qt.application.state === Qt.ApplicationActive) {
root.visible = true
refreshTimer.restart()
}
}
}
}
// Hide the window immediately when Android Activity.onPause() fires so that
// Qt's render loop stops before the EGL surface is disconnected. This
// prevents "QRhiGles2: Failed to make context current" and the resulting
// black screen that appears after swiping home and returning.
Connections {
target: SettingsController
function onActivityPaused() {
if (Qt.platform.os === "android") root.visible = false
}
function onActivityResumed() {
if (Qt.platform.os === "android") root.visible = true
}
}
Timer {
id: refreshTimer
interval: 150
repeat: false
onTriggered: {
if (Qt.platform.os === "android" && PageController.isEdgeToEdgeEnabled()) {
console.log("QML: Application resumed with edge-to-edge")
}
}
}
visible: true
width: GC.screenWidth
height: GC.screenHeight
minimumWidth: GC.isDesktop() ? 360 : 0
minimumHeight: GC.isDesktop() ? 640 : 0
maximumWidth: 600
maximumHeight: 800
color: AmneziaStyle.color.midnightBlack
onClosing: function(close) {
close.accepted = false
PageController.closeWindow()
}
onSceneGraphError: function(error, message) {
// Prevent qFatal crash on Android when EGL context is lost
console.warn("Scene graph error:", error, message)
}
title: "AmneziaVPN"
Item { // This item is needed for focus handling
id: defaultFocusItem
objectName: "defaultFocusItem"
focus: true
Keys.onPressed: function(event) {
switch (event.key) {
case Qt.Key_Tab:
case Qt.Key_Down:
case Qt.Key_Right:
FocusController.nextKeyTabItem()
break
case Qt.Key_Backtab:
case Qt.Key_Up:
case Qt.Key_Left:
FocusController.previousKeyTabItem()
break
default:
PageController.keyPressEvent(event.key)
event.accepted = true
}
}
}
Loader {
active: Qt.platform.os === "android"
source: Qt.platform.os === "android" ? "Components/GamepadLoader.qml" : ""
}
Connections {
objectName: "pageControllerConnections"
target: PageController
function onRaiseMainWindow() {
root.show()
root.raise()
root.requestActivate()
}
function onHideMainWindow() {
root.hide()
}
function onShowErrorMessage(errorMessage) {
popupErrorMessage.text = errorMessage
popupErrorMessage.open()
}
function onShowNotificationMessage(message) {
popupNotificationMessage.text = message
popupNotificationMessage.closeButtonVisible = false
popupNotificationMessage.open()
popupNotificationTimer.start()
}
function onShowPassphraseRequestDrawer() {
privateKeyPassphraseDrawer.openTriggered()
}
function onGoToPageSettingsBackup() {
PageController.goToPage(PageEnum.PageSettingsBackup)
}
function onShowBusyIndicator(visible) {
busyIndicator.visible = visible
PageController.disableControls(visible)
}
function onShowChangelogDrawer() {
changelogDrawer.openTriggered()
}
}
Connections {
objectName: "settingsControllerConnections"
target: SettingsController
function onChangeSettingsFinished(finishedMessage) {
PageController.showNotificationMessage(finishedMessage)
}
}
PageStart {
objectName: "pageStart"
width: root.width
height: root.height
}
Item {
objectName: "popupNotificationItem"
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
implicitHeight: popupNotificationMessage.height
PopupType {
id: popupNotificationMessage
}
Timer {
id: popupNotificationTimer
interval: 3000
repeat: false
running: false
onTriggered: {
popupNotificationMessage.close()
}
}
}
Item {
objectName: "popupErrorMessageItem"
anchors.right: parent.right
anchors.left: parent.left
anchors.bottom: parent.bottom
implicitHeight: popupErrorMessage.height
PopupType {
id: popupErrorMessage
}
}
Item {
objectName: "privateKeyPassphraseDrawerItem"
anchors.fill: parent
DrawerType2 {
id: privateKeyPassphraseDrawer
anchors.fill: parent
expandedHeight: root.height * 0.35 + PageController.safeAreaBottomMargin + PageController.imeHeight
expandedStateContent: ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
anchors.leftMargin: 16
anchors.rightMargin: 16
Connections {
target: privateKeyPassphraseDrawer
function onOpened() {
passphrase.textField.text = ""
passphrase.textField.forceActiveFocus()
}
function onAboutToHide() {
if (passphrase.textField.text !== "") {
PageController.showBusyIndicator(true)
}
}
function onAboutToShow() {
PageController.showBusyIndicator(false)
}
}
TextFieldWithHeaderType {
id: passphrase
property bool hidePassword: true
Layout.fillWidth: true
headerText: qsTr("Private key passphrase")
textField.echoMode: hidePassword ? TextInput.Password : TextInput.Normal
buttonImageSource: hidePassword ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg"
clickedFunc: function() {
hidePassword = !hidePassword
}
}
BasicButtonType {
id: saveButton
Layout.fillWidth: true
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Save")
clickedFunc: function() {
privateKeyPassphraseDrawer.closeTriggered()
PageController.passphraseRequestDrawerClosed(passphrase.textField.text)
}
}
}
}
}
Item {
objectName: "questionDrawerItem"
anchors.fill: parent
QuestionDrawer {
id: questionDrawer
anchors.fill: parent
}
}
Item {
objectName: "subscriptionExpiredDrawerItem"
anchors.fill: parent
SubscriptionExpiredDrawer {
id: subscriptionExpiredDrawer
anchors.fill: parent
}
}
Connections {
target: SubscriptionUiController
function onSubscriptionExpiredOnServer() {
subscriptionExpiredDrawer.openTriggered()
}
}
Connections {
target: SubscriptionUiController
function onRenewalLinkReceived(url) {
Qt.openUrlExternally(url)
}
}
Item {
objectName: "busyIndicatorItem"
anchors.fill: parent
BusyIndicatorType {
id: busyIndicator
anchors.centerIn: parent
z: 1
}
}
function showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) {
questionDrawer.headerText = headerText
questionDrawer.descriptionText = descriptionText
questionDrawer.yesButtonText = yesButtonText
questionDrawer.noButtonText = noButtonText
questionDrawer.yesButtonFunction = function() {
questionDrawer.closeTriggered()
if (yesButtonFunction && typeof yesButtonFunction === "function") {
yesButtonFunction()
}
}
questionDrawer.noButtonFunction = function() {
questionDrawer.closeTriggered()
if (noButtonFunction && typeof noButtonFunction === "function") {
noButtonFunction()
}
}
questionDrawer.openTriggered()
}
FileDialog {
id: mainFileDialog
objectName: "mainFileDialog"
property bool isSaveMode: false
fileMode: isSaveMode ? FileDialog.SaveFile : FileDialog.OpenFile
onAccepted: SystemController.fileDialogClosed(true)
onRejected: SystemController.fileDialogClosed(false)
}
Item {
anchors.fill: parent
ChangelogDrawer {
id: changelogDrawer
anchors.fill: parent
}
}
}