add qml QR Code

This commit is contained in:
dranik
2026-05-07 21:51:39 +03:00
parent 5beae954c7
commit 2cb12c596c
14 changed files with 525 additions and 17 deletions
+4 -2
View File
@@ -34,7 +34,9 @@ add_definitions(-DDEV_S3_ENDPOINT="$ENV{DEV_S3_ENDPOINT}")
add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}") add_definitions(-DFREE_V2_ENDPOINT="$ENV{FREE_V2_ENDPOINT}")
add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}") add_definitions(-DPREM_V1_ENDPOINT="$ENV{PREM_V1_ENDPOINT}")
if(AMNEZIA_QR_PAIRING_ALLOW)
include(../.cache/agw_rsa_public_keys.cmake) include(../.cache/agw_rsa_public_keys.cmake)
endif()
if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID)) if(WIN32 OR (APPLE AND NOT IOS) OR (LINUX AND NOT ANDROID))
set(PACKAGES ${PACKAGES} Widgets) set(PACKAGES ${PACKAGES} Widgets)
@@ -204,8 +206,8 @@ list(APPEND SOURCES ${CMAKE_CURRENT_LIST_DIR}/main.cpp)
target_link_libraries(${PROJECT} PRIVATE ${LIBS}) target_link_libraries(${PROJECT} PRIVATE ${LIBS})
target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>") target_compile_definitions(${PROJECT} PRIVATE "MZ_$<UPPER_CASE:${MZ_PLATFORM_NAME}>")
if(AMNEZIA_LOCAL_GATEWAY) if(AMNEZIA_QR_PAIRING_ALLOW)
target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_LOCAL_GATEWAY) target_compile_definitions(${PROJECT} PRIVATE AMNEZIA_QR_PAIRING_ALLOW)
endif() endif()
if(AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY) if(AMNEZIA_QR_PAIRING_ALLOW_DUPLICATE_VPN_KEY)
+4
View File
@@ -251,6 +251,10 @@ bool AmneziaApplication::parseCommands()
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE) #if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
void AmneziaApplication::startLocalServer() { void AmneziaApplication::startLocalServer() {
#ifdef AMNEZIA_QR_PAIRING_ALLOW
return;
#endif
const QString serverName("AmneziaVPNInstance"); const QString serverName("AmneziaVPNInstance");
QLocalServer::removeServer(serverName); QLocalServer::removeServer(serverName);
@@ -86,7 +86,7 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
} }
#endif #endif
#ifdef AMNEZIA_LOCAL_GATEWAY #ifdef AMNEZIA_QR_PAIRING_ALLOW
{ {
const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl); const QUrl gatewayUrl(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl);
const QString host = gatewayUrl.host().toLower(); const QString host = gatewayUrl.host().toLower();
@@ -16,7 +16,7 @@
using namespace amnezia; using namespace amnezia;
namespace { namespace {
#ifdef AMNEZIA_LOCAL_GATEWAY #ifdef AMNEZIA_QR_PAIRING_ALLOW
// Prefer 127.0.0.1 with local mock (tools/local_gateway listens on 0.0.0.0:8080); avoids LAN/IPv6 ambiguity in dev. // Prefer 127.0.0.1 with local mock (tools/local_gateway listens on 0.0.0.0:8080); avoids LAN/IPv6 ambiguity in dev.
constexpr char gatewayEndpoint[] = "http://127.0.0.1:8080/"; constexpr char gatewayEndpoint[] = "http://127.0.0.1:8080/";
#else #else
@@ -143,6 +143,15 @@ QString PairingUiController::tvStatusMessage() const
return m_tvStatusMessage; return m_tvStatusMessage;
} }
int PairingUiController::tvPairingWaitWindowSeconds() const
{
if (!m_pairingController) {
return 30;
}
const int msec = m_pairingController->pairingLongPollTimeoutMsecs();
return qMax(1, (msec + 999) / 1000);
}
bool PairingUiController::phonePairingBusy() const bool PairingUiController::phonePairingBusy() const
{ {
return m_phonePairingBusy; return m_phonePairingBusy;
@@ -24,6 +24,8 @@ class PairingUiController : public QObject
Q_PROPERTY(QString tvSessionUuid READ tvSessionUuid NOTIFY tvSessionUuidChanged) Q_PROPERTY(QString tvSessionUuid READ tvSessionUuid NOTIFY tvSessionUuidChanged)
Q_PROPERTY(bool tvPairingBusy READ tvPairingBusy NOTIFY tvPairingBusyChanged) Q_PROPERTY(bool tvPairingBusy READ tvPairingBusy NOTIFY tvPairingBusyChanged)
Q_PROPERTY(QString tvStatusMessage READ tvStatusMessage NOTIFY tvStatusMessageChanged) Q_PROPERTY(QString tvStatusMessage READ tvStatusMessage NOTIFY tvStatusMessageChanged)
/** Long-poll window for generate_qr (seconds), for receive UI countdown. */
Q_PROPERTY(int tvPairingWaitWindowSeconds READ tvPairingWaitWindowSeconds NOTIFY tvQrCodesChanged)
Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged) Q_PROPERTY(bool phonePairingBusy READ phonePairingBusy NOTIFY phonePairingBusyChanged)
Q_PROPERTY(QString phoneStatusMessage READ phoneStatusMessage NOTIFY phoneStatusMessageChanged) Q_PROPERTY(QString phoneStatusMessage READ phoneStatusMessage NOTIFY phoneStatusMessageChanged)
@@ -41,6 +43,7 @@ public:
QString tvSessionUuid() const; QString tvSessionUuid() const;
bool tvPairingBusy() const; bool tvPairingBusy() const;
QString tvStatusMessage() const; QString tvStatusMessage() const;
int tvPairingWaitWindowSeconds() const;
bool phonePairingBusy() const; bool phonePairingBusy() const;
QString phoneStatusMessage() const; QString phoneStatusMessage() const;
+3 -1
View File
@@ -80,7 +80,9 @@ namespace PageLoader
PageSetupWizardApiPremiumInfo, PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail, PageSetupWizardApiTrialEmail,
PageSettingsApiQrPairing, PageSettingsApiQrPairingDev,
PageSettingsApiQrPairingSend,
PageSetupWizardApiQrPairingReceive,
PageDevMenu PageDevMenu
}; };
+16 -1
View File
@@ -90,12 +90,27 @@ PageType {
footer: ColumnLayout { footer: ColumnLayout {
width: listView.width width: listView.width
SwitcherType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 24 Layout.topMargin: 24
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.leftMargin: 16 Layout.leftMargin: 16
text: qsTr("QR pairing (full dev UI)")
descriptionText: qsTr("Receive + send on one device for local gateway / QA")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsApiQrPairingDev)
}
}
SwitcherType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Dev gateway environment") text: qsTr("Dev gateway environment")
checked: SettingsController.isDevGatewayEnv checked: SettingsController.isDevGatewayEnv
onToggled: function() { onToggled: function() {
@@ -3,7 +3,6 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QRCodeReader 1.0 import QRCodeReader 1.0
import PageEnum 1.0
import Style 1.0 import Style 1.0
import "../Controls2" import "../Controls2"
@@ -46,13 +45,22 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
Layout.topMargin: 8 Layout.topMargin: 8
text: qsTr("QR pairing") text: qsTr("QR pairing (dev — single device)")
font.pixelSize: 28 font.pixelSize: 28
font.bold: true font.bold: true
color: AmneziaStyle.color.paleGray color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
color: AmneziaStyle.color.goldenApricot
text: qsTr("Developer / QA: receive and send on one device (e.g. with local gateway). Not shown in production menus unless opened from Dev menu.")
wrapMode: Text.Wrap
}
ParagraphTextType { ParagraphTextType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: 16 Layout.leftMargin: 16
@@ -88,8 +96,6 @@ PageType {
Layout.leftMargin: 16 Layout.leftMargin: 16
Layout.rightMargin: 16 Layout.rightMargin: 16
text: qsTr("Cancel receive") text: qsTr("Cancel receive")
// Do not use defaultColor: transparent here: when enabled, BasicButtonType paints that
// as the idle background, so midnightBlack label sits on the page invisible until hover.
enabled: PairingUiController.tvPairingBusy enabled: PairingUiController.tvPairingBusy
clickedFunc: function() { clickedFunc: function() {
PairingUiController.cancelTvQrSession() PairingUiController.cancelTvQrSession()
@@ -105,7 +111,6 @@ PageType {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
// SVG QR from qrCodeUtils has a tiny viewBox (~45px); without a sized container + sourceSize it stays small.
Item { Item {
id: qrBox id: qrBox
Layout.fillWidth: true Layout.fillWidth: true
@@ -182,7 +187,6 @@ PageType {
visible: Layout.preferredHeight > 0 visible: Layout.preferredHeight > 0
clip: true clip: true
// QRCodeReader is a QObject (not Item): no anchors; preview rect via setCameraSize like PageSetupWizardQrReader.
QRCodeReader { QRCodeReader {
id: pairingQrReader id: pairingQrReader
@@ -246,11 +250,22 @@ PageType {
} }
function onTvPairingConfigReceived() { function onTvPairingConfigReceived() {
root.pairingCameraOpen = false
pairingQrReader.stopReading()
qrImage.source = ""
PageController.showNotificationMessage(qsTr("Configuration received from gateway")) PageController.showNotificationMessage(qsTr("Configuration received from gateway"))
Qt.callLater(function() {
PageController.closePage()
})
} }
function onPhonePairingSucceeded() { function onPhonePairingSucceeded() {
root.pairingCameraOpen = false
pairingQrReader.stopReading()
PageController.showNotificationMessage(qsTr("Configuration sent")) PageController.showNotificationMessage(qsTr("Configuration sent"))
Qt.callLater(function() {
PageController.closePage()
})
} }
function onPairingUuidFromScan(uuid) { function onPairingUuidFromScan(uuid) {
@@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QRCodeReader 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property bool pairingCameraOpen: false
Connections {
target: root
function onVisibleChanged() {
if (!root.visible) {
pairingQrReader.stopReading()
root.pairingCameraOpen = false
PairingUiController.cancelAllPairingActivity()
}
}
}
FlickableType {
anchors.fill: parent
contentHeight: layout.implicitHeight
ColumnLayout {
id: layout
width: root.width
spacing: 8
BackButtonType {
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
text: qsTr("Transfer subscription (QR)")
font.pixelSize: 28
font.bold: true
color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Scan the session QR shown on the receiving device, then send this servers Amnezia Premium configuration through the gateway.")
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Send from this subscription")
font.pixelSize: 18
font.bold: true
color: AmneziaStyle.color.mutedGray
}
TextFieldWithHeaderType {
id: uuidField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
headerText: qsTr("QR session UUID")
textField.placeholderText: qsTr("Paste UUID from the other devices QR")
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: Qt.platform.os === "android" || Qt.platform.os === "ios"
text: {
if (Qt.platform.os === "ios" && root.pairingCameraOpen) {
return qsTr("Hide camera")
}
return qsTr("Scan QR code")
}
enabled: !PairingUiController.phonePairingBusy
clickedFunc: function() {
if (Qt.platform.os === "android") {
PairingUiController.openPairingQrScanner()
} else {
root.pairingCameraOpen = !root.pairingCameraOpen
}
}
}
Item {
id: cameraSlot
Layout.fillWidth: true
Layout.preferredHeight: (root.pairingCameraOpen && Qt.platform.os === "ios") ? 220 : 0
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: Layout.preferredHeight > 0
clip: true
QRCodeReader {
id: pairingQrReader
onCodeReaded: function(code) {
if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
pairingQrReader.stopReading()
root.pairingCameraOpen = false
PageController.showNotificationMessage(qsTr("Session ID filled from QR"))
}
}
}
onVisibleChanged: {
if (!visible) {
pairingQrReader.stopReading()
return
}
if (Qt.platform.os === "ios") {
Qt.callLater(function() {
var p = cameraSlot.mapToItem(root, 0, 0)
pairingQrReader.setCameraSize(Qt.rect(p.x, p.y, cameraSlot.width, cameraSlot.height))
pairingQrReader.startReading()
})
}
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: PairingUiController.phonePairingBusy ? qsTr("Sending…") : qsTr("Send from current subscription")
enabled: !PairingUiController.phonePairingBusy
clickedFunc: function() {
PairingUiController.submitPhonePairing(uuidField.textField.text, ServersUiController.getProcessedServerIndex())
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24 + PageController.safeAreaBottomMargin
visible: PairingUiController.phoneStatusMessage.length > 0
text: PairingUiController.phoneStatusMessage
wrapMode: Text.Wrap
}
}
}
Connections {
target: PairingUiController
function onPhonePairingSucceeded() {
root.pairingCameraOpen = false
pairingQrReader.stopReading()
PageController.showNotificationMessage(qsTr("Configuration sent"))
Qt.callLater(function() {
PageController.closePage()
})
}
function onPairingUuidFromScan(uuid) {
uuidField.textField.text = uuid
}
}
}
@@ -378,12 +378,12 @@ PageType {
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("QR pairing (beta)") text: qsTr("Transfer by QR (send)")
descriptionText: qsTr("Transfer config via gateway using a QR code") descriptionText: qsTr("Scan the session QR from the receiving device and send this subscription via the gateway")
rightImageSource: "qrc:/images/controls/chevron-right.svg" rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() { clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsApiQrPairing) PageController.goToPage(PageEnum.PageSettingsApiQrPairingSend)
} }
} }
@@ -0,0 +1,264 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
property int qrImageIndex: 0
property int pairingSecondsLeft: 0
function formatMmSs(totalSec) {
if (totalSec <= 0) {
return "0:00"
}
const m = Math.floor(totalSec / 60)
const s = totalSec % 60
return m + (s < 10 ? ":0" : ":") + s
}
function scrollPairingToBottom() {
receiveScroll.contentY = Math.max(0, receiveScroll.contentHeight - receiveScroll.height)
}
Timer {
id: scrollToBottomRetryTimer
interval: 48
repeat: true
property int retries: 0
onTriggered: {
root.scrollPairingToBottom()
retries++
if (retries >= 12) {
stop()
}
}
onRunningChanged: {
if (!running) {
retries = 0
}
}
}
Timer {
id: pairingCountdownTimer
interval: 1000
repeat: true
running: PairingUiController.tvPairingBusy && PairingUiController.tvQrCodesCount > 0 && root.pairingSecondsLeft > 0
onTriggered: {
if (root.pairingSecondsLeft > 0) {
root.pairingSecondsLeft--
}
}
}
Connections {
target: root
function onVisibleChanged() {
if (!root.visible) {
PairingUiController.cancelAllPairingActivity()
scrollToBottomRetryTimer.stop()
pairingCountdownTimer.stop()
root.pairingSecondsLeft = 0
}
}
}
FlickableType {
id: receiveScroll
anchors.fill: parent
contentHeight: layout.implicitHeight
Behavior on contentY {
NumberAnimation {
duration: 320
easing.type: Easing.OutCubic
}
}
onContentHeightChanged: {
if (PairingUiController.tvQrCodesCount > 0) {
Qt.callLater(root.scrollPairingToBottom)
}
}
ColumnLayout {
id: layout
width: root.width
spacing: 8
BackButtonType {
Layout.topMargin: 20 + PageController.safeAreaTopMargin
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
text: qsTr("Get Premium server from mobile")
font.pixelSize: 28
font.bold: true
color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
color: AmneziaStyle.color.goldenApricot
text: qsTr("Amnezia Premium only. Someone who already has this subscription in Amnezia on a phone or tablet must send it to you; otherwise the session expires.")
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 12
text: qsTr("How to get the server from a mobile device")
font.pixelSize: 18
font.bold: true
color: AmneziaStyle.color.mutedGray
wrapMode: Text.Wrap
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("On this device (TV, tablet, or second phone):\n1) In the “Start receiving” section, tap “Start and show QR” and leave this screen open until the transfer finishes or times out.\n\nOn the mobile device that already has Amnezia Premium:\n2) Open Amnezia VPN → Settings (gear).\n3) Select your Amnezia Premium API server in the list, then open its details screen.\n4) Choose “Transfer by QR (send)”.\n5) Scan the QR code shown on this device, or paste the session ID if you copy it from this screen.\n6) Tap “Send from current subscription” and wait. When the gateway completes pairing, this device receives the configuration and adds the server.")
wrapMode: Text.Wrap
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: PairingUiController.tvStatusMessage.length > 0
text: PairingUiController.tvStatusMessage
wrapMode: Text.Wrap
}
Item {
id: qrBox
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.preferredHeight: PairingUiController.tvQrCodesCount > 0 ? width : 0
visible: PairingUiController.tvQrCodesCount > 0
Image {
id: qrImage
anchors.fill: parent
fillMode: Image.PreserveAspectFit
sourceSize: Qt.size(2048, 2048)
source: PairingUiController.tvQrCodesCount > 0 ? PairingUiController.tvQrCodes[root.qrImageIndex] : ""
MouseArea {
anchors.fill: parent
enabled: PairingUiController.tvQrCodesCount > 1
onClicked: {
root.qrImageIndex = (root.qrImageIndex + 1) % PairingUiController.tvQrCodesCount
}
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: PairingUiController.tvPairingBusy && PairingUiController.tvQrCodesCount > 0
color: AmneziaStyle.color.mutedGray
text: qsTr("This QR code will refresh in %1. If the session expires, tap Start again for a new code.")
.arg(root.formatMmSs(root.pairingSecondsLeft))
wrapMode: Text.Wrap
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Start receiving")
font.pixelSize: 18
font.bold: true
color: AmneziaStyle.color.mutedGray
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: PairingUiController.tvPairingBusy ? qsTr("Waiting…") : qsTr("Start and show QR")
enabled: !PairingUiController.tvPairingBusy && !PairingUiController.phonePairingBusy
clickedFunc: function() {
PairingUiController.startTvQrSession()
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Cancel receive")
enabled: PairingUiController.tvPairingBusy
clickedFunc: function() {
PairingUiController.cancelTvQrSession()
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 24 + PageController.safeAreaBottomMargin
}
}
}
Connections {
target: PairingUiController
function onTvQrCodesChanged() {
root.qrImageIndex = 0
if (PairingUiController.tvQrCodesCount > 0) {
root.pairingSecondsLeft = PairingUiController.tvPairingWaitWindowSeconds
scrollToBottomRetryTimer.retries = 0
scrollToBottomRetryTimer.start()
Qt.callLater(function() {
root.scrollPairingToBottom()
})
Qt.callLater(function() {
Qt.callLater(function() {
root.scrollPairingToBottom()
})
})
}
}
function onTvSessionUuidChanged() {
root.qrImageIndex = 0
}
function onTvPairingConfigReceived() {
scrollToBottomRetryTimer.stop()
root.pairingSecondsLeft = 0
qrImage.source = ""
PageController.showNotificationMessage(qsTr("Configuration received from gateway"))
Qt.callLater(function() {
PageController.closePage()
})
}
}
}
@@ -269,6 +269,7 @@ PageType {
selfHostVpn, selfHostVpn,
backupRestore, backupRestore,
fileOpen, fileOpen,
gatewayQrPairingAddServer,
qrScan, qrScan,
restorePurchases, restorePurchases,
siteLink siteLink
@@ -343,6 +344,19 @@ PageType {
} }
} }
QtObject {
id: gatewayQrPairingAddServer
property bool featuredAmneziaConnection: false
property string title: qsTr("Get Premium server from mobile")
property string description: qsTr("Premium · QR transfer — steps inside")
property string imageSource: "qrc:/images/controls/qr-code.svg"
property bool isVisible: true
property var handler: function() {
PageController.goToPage(PageEnum.PageSetupWizardApiQrPairingReceive)
}
}
QtObject { QtObject {
id: qrScan id: qrScan
+3 -1
View File
@@ -85,7 +85,9 @@
<file>Pages2/PageSettingsAbout.qml</file> <file>Pages2/PageSettingsAbout.qml</file>
<file>Pages2/PageSettingsApiAvailableCountries.qml</file> <file>Pages2/PageSettingsApiAvailableCountries.qml</file>
<file>Pages2/PageSettingsApiServerInfo.qml</file> <file>Pages2/PageSettingsApiServerInfo.qml</file>
<file>Pages2/PageSettingsApiQrPairing.qml</file> <file>Pages2/PageSettingsApiQrPairingDev.qml</file>
<file>Pages2/PageSettingsApiQrPairingSend.qml</file>
<file>Pages2/PageSetupWizardApiQrPairingReceive.qml</file>
<file>Pages2/PageSettingsApplication.qml</file> <file>Pages2/PageSettingsApplication.qml</file>
<file>Pages2/PageSettingsAppSplitTunneling.qml</file> <file>Pages2/PageSettingsAppSplitTunneling.qml</file>
<file>Pages2/PageSettingsBackup.qml</file> <file>Pages2/PageSettingsBackup.qml</file>