2026-05-07 14:34:40 +03:00
|
|
|
|
import QtQuick
|
2026-05-08 21:35:08 +03:00
|
|
|
|
import QtQuick.Window
|
2026-05-07 14:34:40 +03:00
|
|
|
|
import QtQuick.Controls
|
|
|
|
|
|
import QtQuick.Layouts
|
|
|
|
|
|
|
2026-05-07 19:15:28 +03:00
|
|
|
|
import QRCodeReader 1.0
|
2026-05-08 09:56:04 +03:00
|
|
|
|
import PageEnum 1.0
|
2026-05-07 14:34:40 +03:00
|
|
|
|
import Style 1.0
|
|
|
|
|
|
|
2026-05-08 16:57:35 +03:00
|
|
|
|
import "./"
|
2026-05-07 14:34:40 +03:00
|
|
|
|
import "../Controls2"
|
|
|
|
|
|
import "../Controls2/TextTypes"
|
|
|
|
|
|
import "../Config"
|
|
|
|
|
|
import "../Components"
|
|
|
|
|
|
|
|
|
|
|
|
PageType {
|
|
|
|
|
|
id: root
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
/** 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"
|
|
|
|
|
|
|
2026-05-08 22:36:53 +03:00
|
|
|
|
/** iOS-only: full-screen UIKit UIWindow scanner (PairingUiController.iosNativePairingQrOverlayBuild — not Qt.platform.os). */
|
|
|
|
|
|
readonly property bool useIosNativePairingQrOverlay: PairingUiController.iosNativePairingQrOverlayBuild
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
/** 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
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 09:56:04 +03:00
|
|
|
|
/** 0 = scan QR, 1 = confirm before sending subscription */
|
|
|
|
|
|
property int pairingWizardStep: 0
|
|
|
|
|
|
property bool keepPhonePairingInBackgroundOnClose: false
|
2026-05-07 23:37:48 +03:00
|
|
|
|
|
2026-05-08 09:56:04 +03:00
|
|
|
|
property int lastInvalidPairingQrToastClockMs: 0
|
|
|
|
|
|
property bool addDeviceConfirmNavigationScheduled: false
|
2026-05-08 16:57:35 +03:00
|
|
|
|
property bool awaitingCameraPermissionForScan: false
|
|
|
|
|
|
property bool waitingSettingsReturnForScan: false
|
2026-05-08 21:35:08 +03:00
|
|
|
|
property bool torchOn: false
|
2026-05-07 19:15:28 +03:00
|
|
|
|
|
2026-05-07 22:50:14 +03:00
|
|
|
|
Timer {
|
|
|
|
|
|
id: pairingCameraKickTimer
|
2026-05-08 21:35:08 +03:00
|
|
|
|
interval: 220
|
2026-05-07 22:50:14 +03:00
|
|
|
|
repeat: false
|
|
|
|
|
|
onTriggered: root.restartPairingIosCamera()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
function stopMobileScanner() {
|
|
|
|
|
|
torchOn = false
|
2026-05-08 22:36:53 +03:00
|
|
|
|
if (root.useIosNativePairingQrOverlay) {
|
|
|
|
|
|
PairingUiController.setPairingQrTorchEnabled(false)
|
|
|
|
|
|
PairingUiController.dismissIosPairingQrNativeOverlayScanner()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 16:57:35 +03:00
|
|
|
|
if (Qt.platform.os === "android") {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
PairingUiController.setPairingQrTorchEnabled(false)
|
|
|
|
|
|
} else if (root.useIosStyleNativeQrReader) {
|
|
|
|
|
|
pairingQrReader.setTorchEnabled(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
pairingQrReader.stopReading()
|
|
|
|
|
|
PairingUiController.embeddedPairingQrCameraActive = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function startMobileScanner() {
|
|
|
|
|
|
if (!GC.isMobile()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 22:50:21 +03:00
|
|
|
|
if (!root.visible) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 22:36:53 +03:00
|
|
|
|
console.warn("[PairingQrSend] startMobileScanner Qt.platform.os=", Qt.platform.os,
|
|
|
|
|
|
"iosNativePairingQrOverlayBuild=", PairingUiController.iosNativePairingQrOverlayBuild,
|
|
|
|
|
|
"useIosNativePairingQrOverlay=", root.useIosNativePairingQrOverlay)
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!PairingUiController.isPairingCameraAccessGranted()) {
|
|
|
|
|
|
awaitingCameraPermissionForScan = true
|
|
|
|
|
|
PairingUiController.requestPairingCameraAccess()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 22:36:53 +03:00
|
|
|
|
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 2–3 taps). */
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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()
|
2026-05-08 22:36:53 +03:00
|
|
|
|
/** After resume embedded can stay true so setEmbedded skips; QUIMetalView may be opaque again. */
|
|
|
|
|
|
Qt.callLater(function () {
|
|
|
|
|
|
PairingUiController.refreshIosEmbeddedPairingQrChrome()
|
|
|
|
|
|
})
|
2026-05-08 16:57:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
function startPairingScanAfterPermission() {
|
|
|
|
|
|
startMobileScanner()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 16:57:35 +03:00
|
|
|
|
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() {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!waitingSettingsReturnForScan || !visible || pairingWizardStep !== 0) {
|
2026-05-08 16:57:35 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (PairingUiController.isPairingCameraAccessGranted()) {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
waitingSettingsReturnForScan = false
|
|
|
|
|
|
startMobileScanner()
|
2026-05-08 16:57:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-07 22:50:14 +03:00
|
|
|
|
function restartPairingIosCamera() {
|
2026-05-08 22:36:53 +03:00
|
|
|
|
if (root.useIosNativePairingQrOverlay) {
|
|
|
|
|
|
PairingUiController.restartIosPairingQrNativeOverlayCapture()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!root.useIosStyleNativeQrReader || pairingWizardStep !== 0) {
|
2026-05-07 22:50:14 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
// 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).
|
2026-05-07 22:50:14 +03:00
|
|
|
|
pairingQrReader.stopReading()
|
|
|
|
|
|
pairingQrReader.startReading()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 09:56:04 +03:00
|
|
|
|
Component.onDestruction: {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
2026-05-08 09:56:04 +03:00
|
|
|
|
PairingUiController.cancelAllPairingActivity()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
onVisibleChanged: {
|
|
|
|
|
|
if (visible) {
|
|
|
|
|
|
addDeviceConfirmNavigationScheduled = false
|
|
|
|
|
|
if (pairingWizardStep === 0) {
|
|
|
|
|
|
Qt.callLater(startMobileScanner)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pairingCameraKickTimer.stop()
|
|
|
|
|
|
stopMobileScanner()
|
|
|
|
|
|
pairingWizardStep = 0
|
|
|
|
|
|
waitingSettingsReturnForScan = false
|
|
|
|
|
|
if (!keepPhonePairingInBackgroundOnClose && !PairingUiController.phonePairingBusy) {
|
|
|
|
|
|
PairingUiController.cancelAllPairingActivity()
|
2026-05-07 19:15:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
onPairingWizardStepChanged: {
|
|
|
|
|
|
if (pairingWizardStep !== 0) {
|
|
|
|
|
|
stopMobileScanner()
|
2026-05-08 22:50:21 +03:00
|
|
|
|
} else if (root.visible) {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
Qt.callLater(startMobileScanner)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 22:36:53 +03:00
|
|
|
|
/**
|
|
|
|
|
|
* 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 16:57:35 +03:00
|
|
|
|
Connections {
|
|
|
|
|
|
target: Qt.application
|
|
|
|
|
|
|
|
|
|
|
|
function onStateChanged() {
|
|
|
|
|
|
if (Qt.application.state !== Qt.ApplicationActive) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
root.tryResumeScanAfterCameraSettings()
|
2026-05-08 22:36:53 +03:00
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-05-08 16:57:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: SettingsController
|
|
|
|
|
|
|
|
|
|
|
|
enabled: Qt.platform.os === "android"
|
|
|
|
|
|
|
|
|
|
|
|
function onActivityResumed() {
|
|
|
|
|
|
root.tryResumeScanAfterCameraSettings()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
Item {
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
|
id: scanStep
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
visible: pairingWizardStep === 0
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-05-07 22:50:14 +03:00
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
|
|
|
|
|
|
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.")
|
2026-05-07 22:50:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 22:50:14 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
/** 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
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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
|
2026-05-08 22:36:53 +03:00
|
|
|
|
/** Native iOS overlay draws its own header. */
|
|
|
|
|
|
visible: !GC.isMobile() || !root.useIosNativePairingQrOverlay
|
2026-05-08 21:35:08 +03:00
|
|
|
|
|
|
|
|
|
|
BackButtonType {
|
|
|
|
|
|
width: parent.width
|
|
|
|
|
|
backButtonFunction: function() {
|
2026-05-08 09:56:04 +03:00
|
|
|
|
PageController.closePage()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
Item {
|
|
|
|
|
|
z: 2
|
|
|
|
|
|
width: 56
|
|
|
|
|
|
height: 56
|
|
|
|
|
|
anchors.bottom: parent.bottom
|
|
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
|
|
anchors.bottomMargin: 28 + PageController.safeAreaBottomMargin
|
2026-05-08 22:36:53 +03:00
|
|
|
|
visible: GC.isMobile() && !root.useIosNativePairingQrOverlay
|
2026-05-08 21:35:08 +03:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-05-08 22:36:53 +03:00
|
|
|
|
} else if (root.useIosNativePairingQrOverlay) {
|
|
|
|
|
|
PairingUiController.setPairingQrTorchEnabled(root.torchOn)
|
2026-05-08 21:35:08 +03:00
|
|
|
|
} else if (root.useIosStyleNativeQrReader) {
|
|
|
|
|
|
pairingQrReader.setTorchEnabled(root.torchOn)
|
|
|
|
|
|
}
|
2026-05-08 09:56:04 +03:00
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-05-08 09:56:04 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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: {
|
2026-05-08 22:36:53 +03:00
|
|
|
|
if (root.useIosNativePairingQrOverlay) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!root.useIosStyleNativeQrReader || root.pairingWizardStep !== 0) {
|
|
|
|
|
|
return
|
2026-05-08 09:56:04 +03:00
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
Qt.callLater(function () {
|
|
|
|
|
|
if (root.pairingWizardStep !== 0 || !PairingUiController.isPairingCameraAccessGranted()) {
|
2026-05-08 16:57:35 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
PairingUiController.embeddedPairingQrCameraActive = true
|
|
|
|
|
|
pairingQrReader.stopReading()
|
|
|
|
|
|
pairingQrReader.startReading()
|
|
|
|
|
|
})
|
2026-05-07 19:15:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
onCodeReaded: function(code) {
|
|
|
|
|
|
if (addDeviceConfirmNavigationScheduled) {
|
|
|
|
|
|
return
|
2026-05-08 09:56:04 +03:00
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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 device’s “receive config” screen."))
|
2026-05-08 09:56:04 +03:00
|
|
|
|
}
|
2026-05-07 19:15:28 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-07 19:15:28 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
ColumnLayout {
|
|
|
|
|
|
id: confirmStep
|
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
|
anchors.leftMargin: 0
|
|
|
|
|
|
anchors.rightMargin: 0
|
|
|
|
|
|
visible: pairingWizardStep === 1
|
|
|
|
|
|
spacing: 16
|
2026-05-08 09:56:04 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
BackButtonType {
|
|
|
|
|
|
Layout.topMargin: 20 + PageController.safeAreaTopMargin
|
|
|
|
|
|
Layout.leftMargin: 0
|
|
|
|
|
|
backButtonFunction: function() {
|
|
|
|
|
|
PairingUiController.cancelAllPairingActivity()
|
|
|
|
|
|
pairingWizardStep = 0
|
|
|
|
|
|
addDeviceConfirmNavigationScheduled = false
|
|
|
|
|
|
Qt.callLater(startMobileScanner)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-08 09:56:04 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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()
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-08 09:56:04 +03:00
|
|
|
|
|
2026-05-08 21:35:08 +03:00
|
|
|
|
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() {
|
|
|
|
|
|
PairingUiController.cancelAllPairingActivity()
|
|
|
|
|
|
pairingWizardStep = 0
|
|
|
|
|
|
addDeviceConfirmNavigationScheduled = false
|
|
|
|
|
|
Qt.callLater(startMobileScanner)
|
2026-05-08 09:56:04 +03:00
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
|
Layout.fillHeight: true
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Connections {
|
|
|
|
|
|
target: PairingUiController
|
|
|
|
|
|
|
2026-05-08 16:57:35 +03:00
|
|
|
|
function onPairingCameraAccessFinished(granted) {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (!awaitingCameraPermissionForScan) {
|
2026-05-08 16:57:35 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
awaitingCameraPermissionForScan = false
|
2026-05-08 16:57:35 +03:00
|
|
|
|
if (granted) {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
startMobileScanner()
|
2026-05-08 16:57:35 +03:00
|
|
|
|
} else {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
waitingSettingsReturnForScan = true
|
|
|
|
|
|
showScanCameraDeniedDrawer()
|
2026-05-08 16:57:35 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 09:56:04 +03:00
|
|
|
|
function onPairingUuidFromScan(uuid) {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
if (addDeviceConfirmNavigationScheduled) {
|
2026-05-08 09:56:04 +03:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-08 21:35:08 +03:00
|
|
|
|
addDeviceConfirmNavigationScheduled = true
|
|
|
|
|
|
stopMobileScanner()
|
2026-05-08 09:56:04 +03:00
|
|
|
|
PairingUiController.pendingPhonePairingUuid = uuid
|
2026-05-07 21:51:39 +03:00
|
|
|
|
Qt.callLater(function() {
|
2026-05-08 21:35:08 +03:00
|
|
|
|
pairingWizardStep = 1
|
2026-05-07 21:51:39 +03:00
|
|
|
|
})
|
2026-05-07 14:34:40 +03:00
|
|
|
|
}
|
2026-05-08 22:36:53 +03:00
|
|
|
|
|
|
|
|
|
|
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 device’s “receive config” screen."))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function onPairingIosNativeQrOverlayBackRequested() {
|
|
|
|
|
|
stopMobileScanner()
|
|
|
|
|
|
PageController.closePage()
|
|
|
|
|
|
}
|
2026-05-07 14:34:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|