Files
amnezia-client/client/ui/qml/Pages2/PageSettingsApiQrPairingSend.qml
T
2026-05-09 17:11:29 +03:00

850 lines
35 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import QRCodeReader 1.0
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
/** Loud dim colors when true (red/blue/cyan/orange regions). Sync with PageStart.pairingQrChromeDebug. */
property bool pairingQrChromeDebug: false
/** iOS (and any non-Android mobile): native QRCodeReader; Qt may not always report os === "ios". */
readonly property bool useIosStyleNativeQrReader: GC.isMobile() && Qt.platform.os !== "android"
/** iOS-only: full-screen UIKit UIWindow scanner (PairingUiController.iosNativePairingQrOverlayBuild — not Qt.platform.os). */
readonly property bool useIosNativePairingQrOverlay: PairingUiController.iosNativePairingQrOverlayBuild
/** Android: full-screen CameraActivity — Qt cannot reliably composite CameraX under QML on some OEMs (e.g. Samsung). */
readonly property bool useAndroidNativePairingQrScanner: GC.isMobile() && Qt.platform.os === "android"
/** Android: iOS-like flow — titles and camera preview only in CameraActivity; QML hides duplicate scan chrome. */
readonly property bool useAndroidNativePairingQrOverlay: PairingUiController.androidNativePairingQrOverlayBuild
&& GC.isMobile()
&& Qt.platform.os === "android"
/** Let dimming draw into window chrome (status bar + tab bar) when camera underlay is active. */
readonly property bool extendScanDimToScreenEdges: GC.isMobile() && pairingWizardStep === 0
&& PairingUiController.embeddedPairingQrCameraActive
&& !root.useAndroidNativePairingQrOverlay
clip: !extendScanDimToScreenEdges
/** QQuickWindow (not Item); do not type as Item — breaks binding on Qt 6. */
readonly property var appWindow: Window.window
/** Pixels of window above this page (status bar / safe area gap). */
readonly property real scanDimBleedTop: {
if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem)
return 0
let bleed = Math.max(0, root.mapToItem(appWindow.contentItem, 0, 0).y)
if (bleed < 2 && root.useIosStyleNativeQrReader)
bleed = Math.max(bleed, PageController.safeAreaTopMargin)
return bleed
}
/** Pixels of window below this page (tab bar + home indicator). */
readonly property real scanDimBleedBottom: {
if (!extendScanDimToScreenEdges || !appWindow || !appWindow.contentItem)
return 0
const o = root.mapToItem(appWindow.contentItem, 0, root.height)
let bleed = Math.max(0, appWindow.height - o.y)
const slack = Math.max(0, appWindow.height - root.height - scanDimBleedTop)
if (bleed < slack - 1)
bleed = Math.max(bleed, slack)
if (bleed < 2 && root.useIosStyleNativeQrReader)
bleed = Math.max(bleed, PageController.safeAreaBottomMargin + 72)
return bleed
}
/**
* Bottom bleed for dimLayer only. On iOS embedded native QR, keep dim inside the page — semi-opaque dim
* extended into the tab stack seam composites badly with opaque tab chrome (persistent hairline).
* Native bottom mask still uses scanDimBleedBottom via pushIosNativeBottomBleedSync().
*/
readonly property real scanDimBleedBottomForDimLayer: (root.useIosStyleNativeQrReader
&& PairingUiController.embeddedPairingQrCameraActive) ? 0 : scanDimBleedBottom
/** iOS: extend UIKit bottom dim under QML tab bar (see iosPairingCameraAccess + PairingUiController). */
function pushIosNativeBottomBleedSync() {
if (!root.useIosStyleNativeQrReader || !PairingUiController.embeddedPairingQrCameraActive) {
return
}
PairingUiController.syncIosEmbeddedPairingQrNativeBottomExtra(Math.max(0, Math.round(root.scanDimBleedBottom)))
}
onScanDimBleedBottomChanged: {
if (PairingUiController.embeddedPairingQrCameraActive && root.useIosStyleNativeQrReader) {
Qt.callLater(root.pushIosNativeBottomBleedSync)
}
}
Timer {
id: pairingScanLayoutLogTimer
interval: 50
repeat: false
onTriggered: root.logPairingScanLayout("timer")
}
Connections {
target: PairingUiController
function onEmbeddedPairingQrCameraActiveChanged() {
if (PairingUiController.embeddedPairingQrCameraActive) {
pairingScanLayoutLogTimer.restart()
Qt.callLater(root.pushIosNativeBottomBleedSync)
}
}
}
function logPairingScanLayout(tag) {
const w = Window.window
const ci = w && w.contentItem ? w.contentItem : null
let m00 = null
let m0h = null
let scanBot = null
let dimTL = null
let dimBR = null
let dimHoleB = null
if (ci) {
m00 = root.mapToItem(ci, 0, 0)
m0h = root.mapToItem(ci, 0, root.height)
scanBot = scanStep.mapToItem(ci, 0, scanStep.height)
dimTL = dimLayer.mapToItem(ci, 0, 0)
dimBR = dimLayer.mapToItem(ci, dimLayer.width, dimLayer.height)
dimHoleB = dimLayer.holeBottom
}
console.warn("[PairingQrLayout]", tag,
"extend=", extendScanDimToScreenEdges,
"clip=", clip,
"root=", root.width, "x", root.height,
"win=", w ? w.width : -1, "x", w ? w.height : -1,
"contentItem=", ci ? ci.width : -1, "x", ci ? ci.height : -1,
"bleedT/B=", scanDimBleedTop, scanDimBleedBottom,
"dimLayerBleedB=", root.scanDimBleedBottomForDimLayer,
"safeT/B=", PageController.safeAreaTopMargin, PageController.safeAreaBottomMargin,
"map00=", m00 ? m00.x + "," + m00.y : "n/a",
"map0h=", m0h ? m0h.x + "," + m0h.y : "n/a",
"ci.scanStepBot=", scanBot ? scanBot.x.toFixed(1) + "," + scanBot.y.toFixed(1) : "n/a",
"ci.dimTL/BR=", dimTL ? dimTL.x.toFixed(1) + "," + dimTL.y.toFixed(1) : "n/a",
dimBR ? dimBR.x.toFixed(1) + "," + dimBR.y.toFixed(1) : "n/a",
"dimHoleB=", dimHoleB !== null ? dimHoleB.toFixed(1) : "n/a",
"win.screen=", w && w.screen ? w.screen.width + "x" + w.screen.height : "n/a",
"dimLayer wh=", dimLayer.width, "x", dimLayer.height)
}
/** 0 = scan QR, 1 = confirm before sending subscription */
property int pairingWizardStep: 0
property bool keepPhonePairingInBackgroundOnClose: false
property int lastInvalidPairingQrToastClockMs: 0
property bool addDeviceConfirmNavigationScheduled: false
property bool awaitingCameraPermissionForScan: false
property bool waitingSettingsReturnForScan: false
property bool torchOn: false
/** Suppress double startActivity when StackView fires both Component.onCompleted and onVisibleChanged. */
property int _androidPairingReaderLastStartMs: 0
Timer {
id: pairingCameraKickTimer
interval: 220
repeat: false
onTriggered: root.restartPairingIosCamera()
}
function stopMobileScanner() {
torchOn = false
if (root.useIosNativePairingQrOverlay) {
PairingUiController.setPairingQrTorchEnabled(false)
PairingUiController.dismissIosPairingQrNativeOverlayScanner()
return
}
if (Qt.platform.os === "android") {
PairingUiController.setPairingQrTorchEnabled(false)
} else if (root.useIosStyleNativeQrReader) {
pairingQrReader.setTorchEnabled(false)
}
pairingQrReader.stopReading()
PairingUiController.embeddedPairingQrCameraActive = false
}
function startMobileScanner() {
if (!GC.isMobile()) {
return
}
if (!root.visible) {
return
}
/** Confirm step (or transition to it): never reopen native / embedded scanner from stray taps or visibility. */
if (root.pairingWizardStep !== 0) {
return
}
if (addDeviceConfirmNavigationScheduled) {
return
}
console.warn("[PairingQrSend] startMobileScanner Qt.platform.os=", Qt.platform.os,
"iosNativePairingQrOverlayBuild=", PairingUiController.iosNativePairingQrOverlayBuild,
"useIosNativePairingQrOverlay=", root.useIosNativePairingQrOverlay,
"androidNativePairingQrOverlayBuild=", PairingUiController.androidNativePairingQrOverlayBuild,
"useAndroidNativePairingQrOverlay=", root.useAndroidNativePairingQrOverlay)
if (!PairingUiController.isPairingCameraAccessGranted()) {
awaitingCameraPermissionForScan = true
PairingUiController.requestPairingCameraAccess()
return
}
if (root.useIosNativePairingQrOverlay) {
PairingUiController.presentIosPairingQrNativeOverlayScanner(
qsTr("Add device via QR"),
qsTr("Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent."))
/** Do not run pairingCameraKickTimer here: restartCapture during first startRunning races the session (torch needs 23 taps). */
return
}
if (root.useAndroidNativePairingQrScanner) {
const coolUntil = PairingUiController.androidPairingReaderCooldownUntilEpochMs
if (Date.now() < coolUntil) {
console.warn("[PairingQrSend] startMobileScanner: skip (native camera cooldown), ms left=",
(coolUntil - Date.now()))
return
}
const now = Date.now()
if (now - _androidPairingReaderLastStartMs < 700) {
console.warn("[PairingQrSend] startMobileScanner: skip duplicate Android CameraActivity within",
(now - _androidPairingReaderLastStartMs), "ms")
return
}
_androidPairingReaderLastStartMs = now
PairingUiController.openPairingQrScanner()
return
}
PairingUiController.embeddedPairingQrCameraActive = true
if (root.useIosStyleNativeQrReader) {
// Session must start here, not only after pairingCameraKickTimer (220ms), otherwise
// torch/scan run before startReading and native layer never attaches.
restartPairingIosCamera()
pairingCameraKickTimer.restart()
/** After resume embedded can stay true so setEmbedded skips; QUIMetalView may be opaque again. */
Qt.callLater(function () {
PairingUiController.refreshIosEmbeddedPairingQrChrome()
})
}
}
function startPairingScanAfterPermission() {
startMobileScanner()
}
function showScanCameraDeniedDrawer() {
showQuestionDrawer(
qsTr("Camera access is required"),
qsTr("Allow camera access to scan the pairing QR code. You can enable it in the system settings for Amnezia VPN."),
qsTr("Open settings"),
qsTr("Cancel"),
function() {
PairingUiController.openPairingCameraAppSettings()
},
function() {
root.waitingSettingsReturnForScan = false
})
}
function tryResumeScanAfterCameraSettings() {
if (!waitingSettingsReturnForScan || !visible || pairingWizardStep !== 0) {
return
}
if (PairingUiController.isPairingCameraAccessGranted()) {
waitingSettingsReturnForScan = false
startMobileScanner()
}
}
function restartPairingIosCamera() {
if (root.useIosNativePairingQrOverlay) {
PairingUiController.restartIosPairingQrNativeOverlayCapture()
return
}
if (!root.useIosStyleNativeQrReader || pairingWizardStep !== 0) {
return
}
// Never gate on root.visible here: under StackView the active page often has
// visible === false while it is on screen, so startReading never ran (no session, no torch).
pairingQrReader.stopReading()
pairingQrReader.startReading()
}
Component.onDestruction: {
if (!keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
PairingUiController.cancelAllPairingActivity()
}
}
onVisibleChanged: {
if (visible) {
/** Only reset confirm flag on scan step; clearing it on confirm breaks guards if visible flickers. */
if (pairingWizardStep === 0) {
addDeviceConfirmNavigationScheduled = false
Qt.callLater(startMobileScanner)
}
} else {
pairingCameraKickTimer.stop()
stopMobileScanner()
_androidPairingReaderLastStartMs = 0
pairingWizardStep = 0
waitingSettingsReturnForScan = false
if (!keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
PairingUiController.cancelAllPairingActivity()
}
}
}
onPairingWizardStepChanged: {
if (pairingWizardStep !== 0) {
stopMobileScanner()
} else if (root.visible) {
/**
* Android native: use Qt.callLater like iOS — a multi-second Timer delay left the QML scan chrome
* visible with an empty (black) viewport until CameraActivity opened.
*/
Qt.callLater(startMobileScanner)
}
}
/**
* StackView often instantiates the page already visible — onVisibleChanged(true) may never run, so
* startMobileScanner (and native overlay present) would be skipped; only stop/dismiss runs. Same pattern as
* PageSetupWizardApiQrPairingReceive (Component.onCompleted + visible).
*/
Component.onCompleted: {
if (GC.isMobile() && root.visible && pairingWizardStep === 0) {
console.warn("[PairingQrSend] Component.onCompleted: schedule startMobileScanner (page created visible)")
Qt.callLater(startMobileScanner)
}
}
Connections {
target: Qt.application
function onStateChanged() {
if (Qt.application.state !== Qt.ApplicationActive) {
return
}
root.tryResumeScanAfterCameraSettings()
if (!root.useIosStyleNativeQrReader || root.pairingWizardStep !== 0
|| !PairingUiController.isPairingCameraAccessGranted()) {
return
}
if (root.useIosNativePairingQrOverlay) {
Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0 || !GC.isMobile()) {
return
}
PairingUiController.restartIosPairingQrNativeOverlayCapture()
})
return
}
/**
* No fixed ms delay: f1 reapply UIView transparency; f2 restart AVCapture; f3 refresh again —
* after restartPairingIosCamera, QUIMetalView / render thread often rebuilds opaque layers (same bug
* as status-bar-only camera) until underlay is reapplied once more.
*/
Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0 || !GC.isMobile()) {
return
}
console.warn("[PairingQrResume] ApplicationActive f1 underlay")
PairingUiController.embeddedPairingQrCameraActive = true
PairingUiController.refreshIosEmbeddedPairingQrChrome()
Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0) {
return
}
console.warn("[PairingQrResume] ApplicationActive f2 restart camera")
root.restartPairingIosCamera()
Qt.callLater(function () {
if (!root.visible || root.pairingWizardStep !== 0) {
return
}
console.warn("[PairingQrResume] ApplicationActive f3 underlay post-camera")
PairingUiController.refreshIosEmbeddedPairingQrChrome()
})
})
})
}
}
Connections {
target: SettingsController
enabled: Qt.platform.os === "android"
function onActivityResumed() {
root.tryResumeScanAfterCameraSettings()
}
}
Item {
anchors.fill: parent
/** Brief Qt backdrop + back while CameraActivity is starting (native holds title/instructions like iOS overlay). */
Rectangle {
anchors.fill: parent
visible: pairingWizardStep === 0 && root.useAndroidNativePairingQrOverlay
color: AmneziaStyle.color.midnightBlack
z: 1
}
BackButtonType {
visible: pairingWizardStep === 0 && root.useAndroidNativePairingQrOverlay
anchors.top: parent.top
anchors.topMargin: PageController.safeAreaTopMargin
anchors.left: parent.left
width: parent.width
z: 2
backButtonFunction: function() {
PageController.closePage()
}
}
Item {
id: scanStep
anchors.fill: parent
visible: pairingWizardStep === 0 && !root.useAndroidNativePairingQrOverlay
/** Extra guard: invisible alone can race one frame on some stacks; deny input off scan step. */
enabled: pairingWizardStep === 0 && !root.useAndroidNativePairingQrOverlay
readonly property real sqSz: Math.floor(Math.min(width, height) * 0.72)
readonly property real sqX: (width - sqSz) / 2
readonly property real sqY: (height - sqSz) / 2 - height * 0.06
readonly property real dimAlpha: 0.55
readonly property color dimTopDebug: "#aa3333"
readonly property color dimBottomDebug: "#33aaff"
readonly property color dimLeftDebug: "#3333ff"
readonly property color dimRightDebug: "#ffaa33"
readonly property int bracketThick: 5
readonly property int bracketLen: Math.max(28, Math.floor(sqSz * 0.13))
readonly property real bracketRadius: bracketThick * 0.5
Rectangle {
anchors.fill: parent
color: AmneziaStyle.color.midnightBlack
visible: !GC.isMobile()
}
Label {
anchors.centerIn: parent
width: parent.width - 48
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
visible: !GC.isMobile()
color: AmneziaStyle.color.mutedGray
font.pixelSize: 15
text: qsTr("QR pairing is available in the mobile app.")
}
Item {
id: dimLayer
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: -root.scanDimBleedTop
anchors.bottom: parent.bottom
anchors.bottomMargin: -root.scanDimBleedBottomForDimLayer
visible: GC.isMobile()
z: 0
readonly property real holeTop: root.scanDimBleedTop + scanStep.sqY
readonly property real holeBottom: holeTop + scanStep.sqSz
Rectangle {
x: 0
y: 0
width: parent.width
height: Math.max(0, dimLayer.holeTop)
color: root.pairingQrChromeDebug ? scanStep.dimTopDebug : Qt.rgba(0, 0, 0, scanStep.dimAlpha)
}
Rectangle {
x: 0
y: dimLayer.holeBottom
width: parent.width
height: Math.max(0, dimLayer.height - dimLayer.holeBottom)
color: root.pairingQrChromeDebug ? scanStep.dimBottomDebug : Qt.rgba(0, 0, 0, scanStep.dimAlpha)
}
Rectangle {
x: 0
y: dimLayer.holeTop
width: Math.max(0, scanStep.sqX)
height: scanStep.sqSz
color: root.pairingQrChromeDebug ? scanStep.dimLeftDebug : Qt.rgba(0, 0, 0, scanStep.dimAlpha)
}
Rectangle {
x: scanStep.sqX + scanStep.sqSz
y: dimLayer.holeTop
width: Math.max(0, parent.width - (scanStep.sqX + scanStep.sqSz))
height: scanStep.sqSz
color: root.pairingQrChromeDebug ? scanStep.dimRightDebug : Qt.rgba(0, 0, 0, scanStep.dimAlpha)
}
}
/** Same onyx as tab bar: bridges dim/camera to TabBar sibling so the seam is not only TabBar.background overlap. */
Rectangle {
id: pairingIosStackBottomChromeBridge
objectName: "pairingIosStackBottomChromeBridge"
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 22
visible: GC.isMobile() && root.useIosStyleNativeQrReader && PairingUiController.embeddedPairingQrCameraActive
color: root.pairingQrChromeDebug ? "#8844ff" : AmneziaStyle.color.onyxBlack
z: 1
}
Item {
x: scanStep.sqX
y: scanStep.sqY
width: scanStep.sqSz
height: scanStep.sqSz
visible: GC.isMobile()
Rectangle {
x: 0
y: 0
width: scanStep.bracketLen
height: scanStep.bracketThick
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: 0
y: 0
width: scanStep.bracketThick
height: scanStep.bracketLen
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: scanStep.sqSz - scanStep.bracketLen
y: 0
width: scanStep.bracketLen
height: scanStep.bracketThick
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: scanStep.sqSz - scanStep.bracketThick
y: 0
width: scanStep.bracketThick
height: scanStep.bracketLen
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: 0
y: scanStep.sqSz - scanStep.bracketThick
width: scanStep.bracketLen
height: scanStep.bracketThick
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: 0
y: scanStep.sqSz - scanStep.bracketLen
width: scanStep.bracketThick
height: scanStep.bracketLen
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: scanStep.sqSz - scanStep.bracketLen
y: scanStep.sqSz - scanStep.bracketThick
width: scanStep.bracketLen
height: scanStep.bracketThick
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
Rectangle {
x: scanStep.sqSz - scanStep.bracketThick
y: scanStep.sqSz - scanStep.bracketLen
width: scanStep.bracketThick
height: scanStep.bracketLen
radius: scanStep.bracketRadius
color: AmneziaStyle.color.paleGray
}
}
Column {
id: headerBlock
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 8 + PageController.safeAreaTopMargin
spacing: 10
z: 2
/** Native iOS overlay draws its own header. */
visible: !GC.isMobile() || !root.useIosNativePairingQrOverlay
BackButtonType {
width: parent.width
backButtonFunction: function() {
PageController.closePage()
}
}
Label {
width: parent.width - 32
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Add device via QR")
font.pixelSize: 28
font.bold: true
color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap
}
ParagraphTextType {
width: parent.width - 32
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Scan the session QR shown on the device you want to add. You will confirm before the subscription is sent.")
wrapMode: Text.Wrap
}
}
Item {
z: 2
width: 56
height: 56
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 28 + PageController.safeAreaBottomMargin
visible: GC.isMobile() && !root.useIosNativePairingQrOverlay
Rectangle {
anchors.fill: parent
radius: 28
color: Qt.rgba(1, 1, 1, root.torchOn ? 0.42 : 0.22)
border.width: root.torchOn ? 2 : 0
border.color: AmneziaStyle.color.goldenApricot
}
Text {
anchors.centerIn: parent
text: "🔦"
font.pixelSize: 26
}
MouseArea {
anchors.fill: parent
onClicked: {
root.torchOn = !root.torchOn
if (Qt.platform.os === "android") {
PairingUiController.setPairingQrTorchEnabled(root.torchOn)
} else if (root.useIosNativePairingQrOverlay) {
PairingUiController.setPairingQrTorchEnabled(root.torchOn)
} else if (root.useIosStyleNativeQrReader) {
pairingQrReader.setTorchEnabled(root.torchOn)
}
}
}
}
ParagraphTextType {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottomMargin: 100 + PageController.safeAreaBottomMargin
anchors.leftMargin: 16
anchors.rightMargin: 16
visible: PairingUiController.phoneStatusMessage.length > 0
text: PairingUiController.phoneStatusMessage
wrapMode: Text.Wrap
z: 2
}
Item {
width: 0
height: 0
visible: false
QRCodeReader {
id: pairingQrReader
// Same idea as PageSetupWizardQrReader: ensure startReading runs even if
// StackView/onVisible timing skips startMobileScanner once.
Component.onCompleted: {
if (root.useIosNativePairingQrOverlay) {
return
}
if (!root.useIosStyleNativeQrReader || root.pairingWizardStep !== 0) {
return
}
Qt.callLater(function () {
if (root.pairingWizardStep !== 0 || !PairingUiController.isPairingCameraAccessGranted()) {
return
}
PairingUiController.embeddedPairingQrCameraActive = true
pairingQrReader.stopReading()
pairingQrReader.startReading()
})
}
onCodeReaded: function(code) {
if (addDeviceConfirmNavigationScheduled) {
return
}
if (PairingUiController.applyScannedTextAsPairingUuid(code)) {
addDeviceConfirmNavigationScheduled = true
stopMobileScanner()
} else {
const now = new Date().getTime()
if (now - lastInvalidPairingQrToastClockMs >= 2200) {
lastInvalidPairingQrToastClockMs = now
PageController.showNotificationMessage(
qsTr("This QR code is not a pairing session. Show the code from the other devices “receive config” screen."))
}
}
}
}
}
}
ColumnLayout {
id: confirmStep
anchors.fill: parent
anchors.leftMargin: 0
anchors.rightMargin: 0
visible: pairingWizardStep === 1
z: 10
spacing: 16
BackButtonType {
Layout.topMargin: 20 + PageController.safeAreaTopMargin
Layout.leftMargin: 0
backButtonFunction: function() {
addDeviceConfirmNavigationScheduled = false
pairingWizardStep = 0
PairingUiController.cancelAllPairingActivity()
}
}
Label {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
text: qsTr("Add a new device to the subscription?")
font.pixelSize: 28
font.bold: true
color: AmneziaStyle.color.paleGray
wrapMode: Text.Wrap
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
text: qsTr("Add Device")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
keepPhonePairingInBackgroundOnClose = true
PairingUiController.submitPhonePairing(PairingUiController.pendingPhonePairingUuid,
ServersUiController.getProcessedServerIndex())
Qt.callLater(function() {
PageController.closePage()
})
}
}
BasicButtonType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.paleGray
borderColor: AmneziaStyle.color.paleGray
borderWidth: 1
text: qsTr("Cancel")
clickedFunc: function() {
addDeviceConfirmNavigationScheduled = false
pairingWizardStep = 0
PairingUiController.cancelAllPairingActivity()
}
}
Item {
Layout.fillHeight: true
}
}
}
Connections {
target: PairingUiController
function onPairingCameraAccessFinished(granted) {
if (!awaitingCameraPermissionForScan) {
return
}
awaitingCameraPermissionForScan = false
if (granted) {
if (root.pairingWizardStep === 0) {
startMobileScanner()
}
} else {
waitingSettingsReturnForScan = true
showScanCameraDeniedDrawer()
}
}
function onPairingUuidFromScan(uuid) {
if (addDeviceConfirmNavigationScheduled) {
return
}
addDeviceConfirmNavigationScheduled = true
stopMobileScanner()
PairingUiController.pendingPhonePairingUuid = uuid
/** Immediate step switch so scan chrome is not hit-testable for another frame (avoids reopening CameraActivity). */
pairingWizardStep = 1
}
function onPairingSendQrScanRejectedInvalidPayload() {
if (!root.useIosNativePairingQrOverlay || root.pairingWizardStep !== 0) {
return
}
const now = new Date().getTime()
if (now - lastInvalidPairingQrToastClockMs >= 2200) {
lastInvalidPairingQrToastClockMs = now
PageController.showNotificationMessage(
qsTr("This QR code is not a pairing session. Show the code from the other devices “receive config” screen."))
}
}
function onPairingIosNativeQrOverlayBackRequested() {
stopMobileScanner()
PageController.closePage()
}
/** Native CameraActivity back: leave pairing flow (same as iOS overlay back). Do NOT reopen scanner. */
function onPairingAndroidNativeQrScannerUserDismissed() {
if (!root.useAndroidNativePairingQrOverlay) {
return
}
stopMobileScanner()
PairingUiController.cancelAllPairingActivity()
addDeviceConfirmNavigationScheduled = false
PageController.closePage()
}
}
}