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: "captchaDialogItem" anchors.fill: parent CaptchaDialogType { id: captchaDialog onCaptchaSolved: function(captchaId, solution) { SubscriptionUiController.onCaptchaSolved(captchaId, solution) } onRefreshCaptchaRequested: function() { SubscriptionUiController.onRefreshCaptchaRequested() } } } 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() } function onCaptchaRequired(captchaId, captchaImageBase64, hint) { captchaDialog.captchaId = captchaId captchaDialog.captchaImageBase64 = captchaImageBase64 captchaDialog.hint = hint captchaDialog.open() } function onCaptchaFlowDismissRequested() { captchaDialog.close() } } 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 } } }