From 14c264e350f0c21868eaf0e3b4684e0b4ec7690c Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Thu, 30 Mar 2023 20:57:18 +0300 Subject: [PATCH] feature(Syncing): Embed QR code scanner for syncing devices on onboarding (#9981) * fix(StatusQrCodeScanner): Improve QR code scanner - Almost async loading - Added camera selector - Added `captureRectangle` property - Add component info to sandbox qr code scanner page - Embed QrCodeScanner into desktop app * Compile and link qzxing as shared library * Hardcode settingCurrentNetwork. Propagate inputConnectionString errors. * Added qzxing libdir to e2e tests ld_library_path --- Makefile | 42 +-- ci/Jenkinsfile.e2e | 2 +- config.nims | 1 + src/app_service/service/devices/service.nim | 26 +- ui/StatusQ/sandbox/QrCodeScannerPage.qml | 157 +++++++++++ ui/StatusQ/sandbox/main.qml | 38 +-- ui/StatusQ/sandbox/qml.qrc | 1 + .../Components/StatusQrCodeScanner.qml | 262 +++++++++++++----- .../src/StatusQ/Controls/StatusComboBox.qml | 5 +- .../Onboarding/views/SyncCodeView.qml | 50 ++-- .../views/sync/SyncDeviceFromDesktop.qml | 2 +- .../views/sync/SyncDeviceFromMobile.qml | 63 ++++- ui/imports/shared/views/DeviceSyncingView.qml | 5 + vendor/qzxing | 2 +- 14 files changed, 484 insertions(+), 172 deletions(-) create mode 100644 ui/StatusQ/sandbox/QrCodeScannerPage.qml diff --git a/Makefile b/Makefile index 120298bacc..982bf4dab5 100644 --- a/Makefile +++ b/Makefile @@ -181,26 +181,31 @@ ifneq ($(detected_OS),Windows) NIM_PARAMS += --passL:"-L$(QT5_LIBDIR)" endif endif - # We manually link QZXing to Nim application, - # because static libraries are not compiled into other static libraries (DOtherSide). - QZXING := vendor/DOtherSide/build/qzxing/libqzxing.a DOTHERSIDE := vendor/DOtherSide/build/lib/libDOtherSideStatic.a DOTHERSIDE_CMAKE_PARAMS += -DENABLE_DYNAMIC_LIBS=OFF -DENABLE_STATIC_LIBS=ON + # We don't create qzxing target, it's build as DOtherSide cmake subdirectory + QZXING := vendor/DOtherSide/build/qzxing/libqzxing.$(LIBSTATUS_EXT) # order matters here, due to "-Wl,-as-needed" - NIM_PARAMS += --passL:"$(DOTHERSIDE)" --passL:"$(QZXING)" --passL:"$(shell PKG_CONFIG_PATH="$(QT5_PCFILEDIR)" pkg-config --libs Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets Qt5Svg Qt5Multimedia)" + NIM_PARAMS += --passL:"$(DOTHERSIDE)" --passL:"$(shell PKG_CONFIG_PATH="$(QT5_PCFILEDIR)" pkg-config --libs Qt5Core Qt5Qml Qt5Gui Qt5Quick Qt5QuickControls2 Qt5Widgets Qt5Svg Qt5Multimedia)" else ifneq ($(QML_DEBUG), false) - QZXING := vendor/DOtherSide/build/qzxing/Debug/qzxing.lib DOTHERSIDE := vendor/DOtherSide/build/lib/Debug/DOtherSide.dll + QZXING := vendor/DOtherSide/build/qzxing/Debug/qzxing.dll else - QZXING := vendor/DOtherSide/build/qzxing/Release/qzxing.lib DOTHERSIDE := vendor/DOtherSide/build/lib/Release/DOtherSide.dll + QZXING := vendor/DOtherSide/build/qzxing/Release/qzxing.dll endif - DOTHERSIDE_CMAKE_PARAMS += -T"v141" -A x64 -DENABLE_DYNAMIC_LIBS=ON -DENABLE_STATIC_LIBS=OFF - NIM_PARAMS += -L:$(DOTHERSIDE) -L:$(QZXING) + + DOTHERSIDE_CMAKE_PARAMS += -T"v141" -A x64 -DENABLE_DYNAMIC_LIBS=ON -DENABLE_STATIC_LIBS=OFF \ + -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=ON # This is for qzxing, as it doesn't export any symbols + NIM_PARAMS += -L:$(DOTHERSIDE) NIM_EXTRA_PARAMS := --passL:"-lsetupapi -lhid" endif +QZXING_LIBDIR := $(shell pwd)/$(shell dirname "$(QZXING)") +export QZXING_LIBDIR + +NIM_PARAMS += --passL:"-L$(QZXING_LIBDIR)" --passL:"-lqzxing" ifeq ($(detected_OS),Darwin) ifeq ("$(shell sysctl -nq hw.optional.arm64)","1") @@ -251,11 +256,7 @@ $(DOTHERSIDE): | deps cd build && \ rm -f CMakeCache.txt && \ cmake $(DOTHERSIDE_CMAKE_PARAMS)\ - -DENABLE_DOCS=OFF \ - -DENABLE_TESTS=OFF \ - -DQZXING_USE_QML=ON \ - -DQZXING_MULTIMEDIA=ON \ - -DQZXING_USE_DECODER_QR_CODE=ON \ + -DENABLE_DOCS=OFF -DENABLE_TESTS=OFF -DQZXING_USE_QML=ON -DQZXING_MULTIMEDIA=ON -DQZXING_USE_DECODER_QR_CODE=ON \ .. $(HANDLE_OUTPUT) && \ $(DOTHERSIDE_BUILD_CMD) @@ -400,6 +401,10 @@ $(NIM_STATUS_CLIENT): $(NIM_SOURCES) $(DOTHERSIDE) | check-qt-dir $(STATUSGO) $( libstatus.dylib \ @rpath/libstatus.dylib \ bin/nim_status_client && \ + install_name_tool -change \ + $(QZXING) \ + @rpath/libqzxing.dylib \ + bin/nim_status_client && \ install_name_tool -change \ libkeycard.dylib \ @rpath/libkeycard.dylib \ @@ -474,6 +479,7 @@ $(STATUS_CLIENT_APPIMAGE): nim_status_client $(APPIMAGE_TOOL) nim-status.desktop cp vendor/status-go/build/bin/libstatus.so tmp/linux/dist/usr/lib/ cp vendor/status-go/build/bin/libstatus.so.0 tmp/linux/dist/usr/lib/ cp $(STATUSKEYCARDGO) tmp/linux/dist/usr/lib/ + cp $(QZXING) tmp/linux/dist/usr/lib/ echo -e $(BUILD_MSG) "AppImage" linuxdeployqt tmp/linux/dist/nim-status.desktop -no-copy-copyright-files -qmldir=ui -qmlimport=$(QT5_QMLDIR) -bundle-non-qt-libs @@ -596,10 +602,12 @@ $(STATUS_CLIENT_EXE): nim_status_client nim_windows_launcher $(NIM_WINDOWS_PREBU cp bin/nim_windows_launcher.exe $(OUTPUT)/Status.exe rcedit $(OUTPUT)/bin/Status.exe --set-icon $(OUTPUT)/resources/status.ico rcedit $(OUTPUT)/Status.exe --set-icon $(OUTPUT)/resources/status.ico - cp $(DOTHERSIDE) $(STATUSGO) $(STATUSKEYCARDGO) tmp/windows/tools/*.dll $(OUTPUT)/bin/ + cp $(DOTHERSIDE) $(QZXING) $(STATUSGO) $(STATUSKEYCARDGO) tmp/windows/tools/*.dll $(OUTPUT)/bin/ cp "$(shell which libgcc_s_seh-1.dll)" $(OUTPUT)/bin/ cp "$(shell which libwinpthread-1.dll)" $(OUTPUT)/bin/ echo -e $(BUILD_MSG) "deployable folder" + windeployqt --compiler-runtime --qmldir ui --release \ + tmp/windows/dist/Status/bin/qzxing.dll windeployqt --compiler-runtime --qmldir ui --release \ tmp/windows/dist/Status/bin/DOtherSide.dll mv tmp/windows/dist/Status/bin/vc_redist.x64.exe tmp/windows/dist/Status/vendor/ @@ -661,7 +669,7 @@ ICON_TOOL := node_modules/.bin/fileicon run-linux: nim_status_client echo -e "\033[92mRunning:\033[39m bin/nim_status_client" - LD_LIBRARY_PATH="$(QT5_LIBDIR)":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)" \ + LD_LIBRARY_PATH="$(QT5_LIBDIR)":"$(QZXING_LIBDIR)":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)" \ ./bin/nim_status_client run-macos: nim_status_client @@ -676,11 +684,11 @@ run-macos: nim_status_client run-windows: nim_status_client $(NIM_WINDOWS_PREBUILT_DLLS) echo -e "\033[92mRunning:\033[39m bin/nim_status_client.exe" - PATH="$(shell pwd)"/"$(shell dirname "$(DOTHERSIDE)")":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)":"$(shell pwd)"/"$(shell dirname "$(NIM_WINDOWS_PREBUILT_DLLS)")":"$(PATH)" \ + PATH="$(shell pwd)"/"$(shell dirname "$(DOTHERSIDE)")":"$(QZXING_LIBDIR)":"$(STATUSGO_LIBDIR)":"$(STATUSKEYCARDGO_LIBDIR)":"$(shell pwd)"/"$(shell dirname "$(NIM_WINDOWS_PREBUILT_DLLS)")":"$(PATH)" \ ./bin/nim_status_client.exe tests-nim-linux: | $(DOTHERSIDE) - LD_LIBRARY_PATH="$(QT5_LIBDIR)" \ + LD_LIBRARY_PATH="$(QT5_LIBDIR)":"$(QZXING_LIBDIR)" \ $(ENV_SCRIPT) nim c $(NIM_PARAMS) $(NIM_EXTRA_PARAMS) -r test/nim/message_model_test.nim statusq-sanity-checker: diff --git a/ci/Jenkinsfile.e2e b/ci/Jenkinsfile.e2e index 2a61758fcb..98822c314a 100644 --- a/ci/Jenkinsfile.e2e +++ b/ci/Jenkinsfile.e2e @@ -54,7 +54,7 @@ pipeline { QTDIR = '/opt/qt/5.15.2/gcc_64' PATH = "${env.QTDIR}/bin:${env.PATH}" /* Include library in order to compile the project */ - LD_LIBRARY_PATH = "$QTDIR/lib:$WORKSPACE/vendor/status-go/build/bin:$WORKSPACE/vendor/status-keycard-go/build/libkeycard/" + LD_LIBRARY_PATH = "$QTDIR/lib:$WORKSPACE/vendor/DOtherSide/build/qzxing:$WORKSPACE/vendor/status-go/build/bin:$WORKSPACE/vendor/status-keycard-go/build/libkeycard/" /* Container ports */ RPC_PORT = "${8545 + env.EXECUTOR_NUMBER.toInteger()}" P2P_PORT = "${6010 + env.EXECUTOR_NUMBER.toInteger()}" diff --git a/config.nims b/config.nims index 7236ca9545..fcab8ef5f4 100644 --- a/config.nims +++ b/config.nims @@ -17,6 +17,7 @@ if defined(macosx): switch("passL", "-rpath" & " " & getEnv("QT5_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("STATUSGO_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("STATUSKEYCARDGO_LIBDIR")) + switch("passL", "-rpath" & " " & getEnv("QZXING_LIBDIR")) # statically link these libs switch("passL", "bottles/openssl@1.1/lib/libcrypto.a") switch("passL", "bottles/openssl@1.1/lib/libssl.a") diff --git a/src/app_service/service/devices/service.nim b/src/app_service/service/devices/service.nim index e1fac84ecb..4cea063545 100644 --- a/src/app_service/service/devices/service.nim +++ b/src/app_service/service/devices/service.nim @@ -74,6 +74,11 @@ QtObject: result.accountsService = accountsService result.localPairingStatus = newLocalPairingStatus() + proc updateLocalPairingStatus(self: Service, data: LocalPairingEventArgs) = + self.events.emit(SIGNAL_LOCAL_PAIRING_EVENT, data) + self.localPairingStatus.update(data.eventType, data.action, data.account, data.error) + self.events.emit(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE, self.localPairingStatus) + proc doConnect(self: Service) = self.events.on(SignalType.Message.event) do(e:Args): let receivedData = MessageSignal(e) @@ -89,10 +94,7 @@ QtObject: action: signalData.action.parse(), account: signalData.account, error: signalData.error) - self.events.emit(SIGNAL_LOCAL_PAIRING_EVENT, data) - - self.localPairingStatus.update(data.eventType, data.action, data.account, data.error) - self.events.emit(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE, self.localPairingStatus) + self.updateLocalPairingStatus(data) proc init*(self: Service) = self.doConnect() @@ -157,8 +159,18 @@ QtObject: # Local Pairing # - proc inputConnectionStringForBootstrappingFinished(self: Service, result: string) = - discard + proc inputConnectionStringForBootstrappingFinished*(self: Service, responseJson: string) {.slot.} = + let response = responseJson.parseJson + let errorDescription = response["error"].getStr + if len(errorDescription) == 0: + return + error "failed to start bootstrapping device", errorDescription + let data = LocalPairingEventArgs( + eventType: EventConnectionError, + action: ActionUnknown, + account: AccountDto(), + error: errorDescription) + self.updateLocalPairingStatus(data) proc validateConnectionString*(self: Service, connectionString: string): string = return status_go.validateConnectionString(connectionString) @@ -187,7 +199,7 @@ QtObject: "deviceType" : hostOs, "nodeConfig": nodeConfigJson, "kdfIterations": self.accountsService.getKdfIterations(), - "settingCurrentNetwork": "" + "settingCurrentNetwork": "mainnet_rpc" }, "clientConfig": %* {} } diff --git a/ui/StatusQ/sandbox/QrCodeScannerPage.qml b/ui/StatusQ/sandbox/QrCodeScannerPage.qml new file mode 100644 index 0000000000..ac17fc757c --- /dev/null +++ b/ui/StatusQ/sandbox/QrCodeScannerPage.qml @@ -0,0 +1,157 @@ +import QtQuick 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import QtMultimedia 5.15 + +Rectangle { + id: root + + component ValueIndicator: RowLayout { + + property string title + property alias value: lastTagText.text + property bool readOnly: true + + spacing: 10 + + StatusBaseText { + text: parent.title + } + + StatusInput { + id: lastTagText + Layout.fillWidth: true + input.edit.readOnly: parent.readOnly + input.edit.selectByKeyboard: true + input.edit.selectByMouse: true + input.rightPadding: 10 + } + } + + color: Theme.palette.baseColor3 + + QtObject { + id: d + + function sizeToString(size) { + return whToString(size.width, size.height) + } + + function whToString(width, height) { + return `${width.toFixed(0)}*${height.toFixed(0)}` + } + + function rectToString(rect) { + return `${rect.width.toFixed(2)}*${rect.height.toFixed(2)} (${rect.x.toFixed(2)}, ${rect.y.toFixed(2)})` + } + + function cameraStateString(state) { + switch (state) { + case Camera.UnloadedState: return "Unloaded" + case Camera.LoadedState: return "Loaded" + case Camera.ActiveState: return "Active" + default: return "unknown" + } + } + + function cameraStatusString(status) { + switch (status) { + case Camera.ActiveStatus: return "Active" + case Camera.StartingStatus: return "Starting" + case Camera.StoppingStatus: return "Stopping" + case Camera.StandbyStatus: return "Standby" + case Camera.LoadedStatus: return "Loaded" + case Camera.LoadingStatus: return "Loading" + case Camera.UnloadingStatus: return "Unloading" + case Camera.UnloadedStatus: return "Unloaded" + case Camera.UnavailableStatus: return "Unavailable" + default: return "unknown" + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 20 + spacing: 10 + + StatusQrCodeScanner { + id: qrScanner + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: width / sourceRatio + } + + ValueIndicator { + Layout.fillWidth: true + title: "Last tag:" + value: qrScanner.lastTag + } + + RowLayout { + Layout.fillWidth: true + + ValueIndicator { + Layout.fillWidth: true + title: "Current tag:" + value: qrScanner.currentTag + } + + ValueIndicator { + title: "Last decode time, ms:" + value: qrScanner.decodeTime + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + ValueIndicator { + Layout.fillWidth: true + title: "Source size:" + value: d.sizeToString(qrScanner.sourceSize) + } + + ValueIndicator { + Layout.fillWidth: true + title: "Source size:" + value: d.sizeToString(qrScanner.contentSize) + } + + ValueIndicator { + Layout.fillWidth: true + title: "View size:" + value: d.whToString(qrScanner.width, qrScanner.height) + } + + ValueIndicator { + Layout.fillWidth: true + title: "Capture rect:" + value: d.rectToString(qrScanner.captureRectangle) + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 10 + + ValueIndicator { + Layout.fillWidth: true + title: "Camera state:" + value: qrScanner.camera ? d.cameraStateString(qrScanner.camera.cameraState) : "" + } + + ValueIndicator { + Layout.fillWidth: true + title: "Camera status:" + value: qrScanner.camera ? d.cameraStatusString(qrScanner.camera.cameraStatus) : "" + } + } + } +} diff --git a/ui/StatusQ/sandbox/main.qml b/ui/StatusQ/sandbox/main.qml index 5a2d4b80f3..9840c29717 100644 --- a/ui/StatusQ/sandbox/main.qml +++ b/ui/StatusQ/sandbox/main.qml @@ -546,44 +546,8 @@ StatusWindow { Component { id: qrScannerComponent - Rectangle { + QrCodeScannerPage { anchors.fill: parent - color: Theme.palette.baseColor3 - - ColumnLayout { - anchors.fill: parent - anchors.margins: 20 - spacing: 10 - - StatusQrCodeScanner { - id: qrScanner - Layout.fillWidth: true - Layout.preferredHeight: width / sourceRatio - } - - RowLayout { - Layout.fillWidth: true - spacing: 10 - - StatusBaseText { - text: qsTr("Last tag: ") - } - - StatusInput { - id: lastTagText - Layout.fillWidth: true - input.enabled: false - text: qrScanner.lastTag - input.rightPadding: 10 - } - - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - } - } } } diff --git a/ui/StatusQ/sandbox/qml.qrc b/ui/StatusQ/sandbox/qml.qrc index 8ad4e56ba5..5f8d76b1f1 100644 --- a/ui/StatusQ/sandbox/qml.qrc +++ b/ui/StatusQ/sandbox/qml.qrc @@ -79,5 +79,6 @@ pages/StatusComboBoxPage.qml pages/StatusChartPanelPage.qml pages/StatusTextAreaPage.qml + QrCodeScannerPage.qml diff --git a/ui/StatusQ/src/StatusQ/Components/StatusQrCodeScanner.qml b/ui/StatusQ/src/StatusQ/Components/StatusQrCodeScanner.qml index 4133c97a5e..56e7962ace 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusQrCodeScanner.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusQrCodeScanner.qml @@ -7,42 +7,112 @@ import QtGraphicalEffects 1.0 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 +import StatusQ.Core.Backpressure 1.0 import StatusQ.Core.Theme 0.1 import QZXing 3.3 +/* + NOTE: I'm doing some crazy workarounds here. Tested on MacOS. + What I wanted to achieve: + + 1. User only gets a OS "allow camera access" popup + when a page with QR code scanner is opened. + 2. Mimize UI freezes, or at least make it obvious + that something is going on. + + Camera component uses main UI thread to request OS for available devices. + Therefore, we can't simply use Loader with `asyncronous` flag. + Neiter we can set `loading: loader.status === Loader.Loading` to this button. + + To achieve desired points, I manually set `loading` property of the button + and delay the camera loading for 250ms. UI quickly shows loading indicator, + then it will freeze until the camera is loaded. + + I think this can only be improved by moving the OS requests to another thread from C++. + + We also don't yet have ability to auto-detect if the camera access was already enabled. + So we show `Enable camera` button everytime. +*/ + Item { id: root - readonly property alias camera: camera + property rect captureRectangle: Qt.rect(0, 0, 1, 1) + // Use this property to clip capture rectangle to biggest possible square + readonly property rect squareCaptureRectangle: { + const size = Math.min(contentSize.width, contentSize.height) + const w = size / contentSize.width + const h = size / contentSize.height + const x = (1 - w) / 2 + const y = (1 - h) / 2 + return Qt.rect(x, y, w, h) + } + + property bool highlightContentZone: false + property bool highlightCaptureZone: false + + readonly property alias camera: d.camera readonly property size sourceSize: Qt.size(videoOutput.sourceRect.width, videoOutput.sourceRect.height) readonly property size contentSize: Qt.size(videoOutput.contentRect.width, videoOutput.contentRect.height) readonly property real sourceRatio: videoOutput.sourceRect.width / videoOutput.sourceRect.height - property int failsCount: 0 - property int tagsCount: 0 - property int decodeTime: 0 + readonly property int failsCount: d.failsCount + readonly property int tagsCount: d.tagsCount + readonly property int decodeTime: d.decodeTime + readonly property string lastTag: d.lastTag + readonly property string currentTag: d.currentTag - property string lastTag + signal tagFound(string tag) implicitWidth: sourceSize.width implicitHeight: sourceSize.height - signal tagFound(string tag) - QtObject { id: d readonly property int radius: 16 + + function setCameraDevice(deviceId) { + if (!d.camera) + return + d.camera.deviceId = "" // Workaround for Qt bug. Without this the device changes only first time. + d.camera.deviceId = deviceId + } + + property QtObject camera: null + + // NOTE: QtMultimedia.availableCameras also makes a request to OS, if not made previously. + // So we postpone this call until the `Camera` component is loaded + property var availableCameras: [] + + function onCameraLoaded() { + d.camera = loader.item + d.availableCameras = QtMultimedia.availableCameras + button.loading = false + } + + property int failsCount: 0 + property int tagsCount: 0 + property int decodeTime: 0 + property string lastTag + property string currentTag + } - Camera { - id: camera - captureMode: Camera.CaptureVideo - focus { - focusMode: CameraFocus.FocusContinuous - focusPointMode: CameraFocus.FocusPointAuto + Loader { + id: loader + active: false + visible: status == Loader.Ready + sourceComponent: Camera { + focus { + focusMode: CameraFocus.FocusContinuous + focusPointMode: CameraFocus.FocusPointAuto + } + } + onLoaded: { + d.onCameraLoaded() } } @@ -56,15 +126,16 @@ Item { anchors.fill: parent implicitWidth: videoOutput.contentRect.width implicitHeight: videoOutput.contentRect.height - visible: camera.availability === Camera.Available + visible: d.camera && d.camera.availability === Camera.Available + clip: true VideoOutput { id: videoOutput anchors.fill: parent visible: false - source: camera + source: d.camera filters: [ qzxingFilter ] - fillMode: VideoOutput.PreserveAspectFit + fillMode: VideoOutput.PreserveAspectCrop } Rectangle { @@ -81,83 +152,138 @@ Item { maskSource: mask } + Loader { + active: root.highlightContentZone + sourceComponent: Rectangle { + color: "blue" + opacity: 0.2 + border.width: 3 + border.color: "blue" + x: videoOutput.contentRect.x + y: videoOutput.contentRect.y + width: videoOutput.contentRect.width + height: videoOutput.contentRect.height + } + } + + Loader { + active: root.highlightCaptureZone + sourceComponent: Rectangle { + color: "hotpink" + opacity: 0.2 + border.width: 3 + border.color: "hotpink" + x: videoOutput.contentRect.x + root.captureRectangle.x * videoOutput.contentRect.width + y: videoOutput.contentRect.y + root.captureRectangle.y * videoOutput.contentRect.height + width: videoOutput.contentRect.width * root.captureRectangle.width + height: videoOutput.contentRect.height * root.captureRectangle.height + } + } + QZXingFilter { id: qzxingFilter orientation: videoOutput.orientation captureRect: { - videoOutput.contentRect; videoOutput.sourceRect; // bindings - const normalizedRectangle = Qt.rect(0, 0, 1, 1) + videoOutput.contentRect; videoOutput.sourceRect // bindings + const normalizedRectangle = root.captureRectangle const rectangle = videoOutput.mapNormalizedRectToItem(normalizedRectangle) return videoOutput.mapRectToSource(rectangle); } decoder { enabledDecoders: QZXing.DecoderFormat_QR_CODE - tryHarder: true onTagFound: { - root.lastTag = tag + d.currentTag = tag + d.lastTag = tag root.tagFound(tag) } } onDecodingFinished: { - if (succeeded) - ++root.tagsCount - else - ++root.failsCount - root.decodeTime = decodeTime + if (succeeded) { + ++d.tagsCount + } else { + ++d.failsCount + d.currentTag = "" + } + d.decodeTime = decodeTime } } } - // TODO: Implement camera selector - // For me it works once. The first switch between 2 cameras is ok. - // The second switch doesn't work and behaves different with 2 approaches: - // With standard `ComboBox` it throws an exception. - // Width `StatusComboBox` it just kinda unbinds from the Camera. - // - // ComboBox { - // id: cameraComboBox + ColumnLayout { + anchors.fill: parent + visible: loader.status !== Loader.Ready || loader.status === Loader.Error + spacing: 20 - // anchors { - // right: parent.right - // bottom: parent.bottom - // margins: 10 - // } + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } - // width: implicitWidth - // opacity: 0.7 - // model: QtMultimedia.availableCameras - // textRole: "displayName" - // valueRole: "deviceId" - // onCurrentValueChanged: { - // camera.deviceId = currentValue - // } - // } + StatusBaseText { + Layout.fillWidth: true + text: qsTr('Enable access to your camera') + leftPadding: 48 + rightPadding: 48 + font.pixelSize: 15 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } - // StatusComboBox { - // id: cameraComboBox - // anchors { - // right: parent.right - // bottom: parent.bottom - // margins: 10 - // } + StatusBaseText { + Layout.fillWidth: true + text: qsTr("To scan a QR, Status needs\naccess to your webcam") + leftPadding: 48 + rightPadding: 48 + font.pixelSize: 15 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.palette.directColor4 + } - // width: implicitWidth - // opacity: 0.7 - // model: QtMultimedia.availableCameras - // control.textRole: "displayName" - // control.valueRole: "deviceId" - // onCurrentValueChanged: { - // console.log("setting deviceId to", currentValue) - // camera.deviceId = currentValue - // } - // } + StatusButton { + id: button + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enable camera access") + onClicked: { + loading = true + Backpressure.debounce(this, 250, () => { loader.active = true })() + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + } + + StatusComboBox { + id: cameraComboBox + + anchors { + right: parent.right + bottom: parent.bottom + margins: 10 + } + + width: Math.min(implicitWidth, parent.width / 2) + visible: Array.isArray(d.availableCameras) && d.availableCameras.length > 0 + opacity: 0.7 + model: d.availableCameras + control.textRole: "displayName" + control.valueRole: "deviceId" + control.padding: 8 + control.spacing: 8 + onCurrentValueChanged: { + // Debounce to close combobox first + Backpressure.debounce(this, 50, () => { d.setCameraDevice(currentValue) })() + } + } ColumnLayout { anchors.verticalCenter: parent.verticalCenter width: parent.width - spacing: 10 StatusBaseText { @@ -165,7 +291,7 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: Theme.palette.dangerColor1 - visible: camera.availability !== Camera.Available + visible: d.camera && d.camera.availability !== Camera.Available text: qsTr("Camera is not available") } @@ -174,8 +300,8 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter color: Theme.palette.directColor5 - visible: camera.errorCode !== Camera.NoError - text: "Error comes here" // camera.errorString + visible: d.camera && d.camera.errorCode !== Camera.NoError + text: d.camera ? d.camera.errorString : "" } } } diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml b/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml index fab1554e5d..cdaef48786 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusComboBox.qml @@ -61,9 +61,6 @@ Item { ComboBox { id: comboBox - property color bgColor: Theme.palette.baseColor2 - property color bgColorHover: bgColor - Layout.fillWidth: true Layout.fillHeight: true Layout.topMargin: labelItem.visible ? 7 : 0 @@ -77,7 +74,7 @@ Item { spacing: 16 background: Rectangle { - implicitHeight: 56 + implicitHeight: 24 + comboBox.topPadding + comboBox.bottomPadding implicitWidth: 448 color: root.type === StatusComboBox.Type.Secondary ? "transparent" : Theme.palette.baseColor2 radius: 8 diff --git a/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml b/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml index 52731b80e1..01a3fb17e7 100644 --- a/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml +++ b/ui/app/AppLayouts/Onboarding/views/SyncCodeView.qml @@ -34,21 +34,26 @@ Item { QtObject { id: d - property string connectionString - function validateConnectionString(connectionString) { const result = root.startupStore.validateLocalPairingConnectionString(connectionString) return result === "" } + + function onConnectionStringFound(connectionString) { + root.startupStore.setConnectionString(connectionString) + root.startupStore.doPrimaryAction() + } } Timer { id: nextStateDelay + + property string connectionString + interval: 1000 repeat: false onTriggered: { - root.startupStore.setConnectionString(d.connectionString) - root.startupStore.doPrimaryAction() + d.onConnectionStringFound(connectionString) } } @@ -84,13 +89,10 @@ Item { } StackLayout { - width: parent.width anchors.horizontalCenter: parent.horizontalCenter - - implicitWidth: Math.max(mobileSync.implicitWidth, desktopSync.implicitWidth) - implicitHeight: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight) + width: parent.width + height: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight) currentIndex: switchTabBar.currentIndex - clip: true // StackLayout doesn't support alignment, so we create an `Item` wrappers @@ -98,17 +100,22 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: mobileSync.implicitWidth - implicitHeight: mobileSync.implicitHeight - SyncDeviceFromMobile { id: mobileSync anchors { + left: parent.left + right: parent.right verticalCenter: parent.verticalCenter - horizontalCenter: parent.horizontalCenter } + validators: [ + StatusValidator { + name: "isSyncQrCode" + errorMessage: qsTr("This does not look like a sync QR code") + validate: d.validateConnectionString + } + ] onConnectionStringFound: { - d.processConnectionString(connectionString) + d.onConnectionStringFound(connectionString) } } } @@ -117,9 +124,6 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: desktopSync.implicitWidth - implicitHeight: desktopSync.implicitHeight - SyncDeviceFromDesktop { id: desktopSync anchors { @@ -130,19 +134,15 @@ Item { input.readOnly: nextStateDelay.running input.validators: [ StatusValidator { - name: "asyncConnectionString" + name: "isSyncCode" errorMessage: qsTr("This does not look like a sync code") - validate: (value) => { - return d.validateConnectionString(value) - } + validate: d.validateConnectionString } ] input.onValidChanged: { - if (!input.valid) { - d.connectionString = "" + if (!input.valid) return - } - d.connectionString = desktopSync.input.text + nextStateDelay.connectionString = desktopSync.input.text nextStateDelay.start() } } diff --git a/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromDesktop.qml b/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromDesktop.qml index d44429b93f..f018d9261a 100644 --- a/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromDesktop.qml +++ b/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromDesktop.qml @@ -10,7 +10,7 @@ Column { StatusSyncCodeInput { id: codeInput - implicitWidth: 400 + width: parent.width mode: StatusSyncCodeInput.WriteMode } } diff --git a/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromMobile.qml b/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromMobile.qml index d94b18d438..13a2bfae26 100644 --- a/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromMobile.qml +++ b/ui/app/AppLayouts/Onboarding/views/sync/SyncDeviceFromMobile.qml @@ -3,29 +3,70 @@ import QtQuick.Layouts 1.12 import QtQuick.Controls 2.13 import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 import StatusQ.Components 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 -Rectangle { +Column { id: root + property list validators + signal connectionStringFound(connectionString: string) - implicitWidth: 330 - implicitHeight: 330 + spacing: 12 - radius: 16 - color: Theme.palette.baseColor4 + QtObject { + id: d + property string errorMessage + property string lastTag + property int counter: 0 + + function validateConnectionString(connectionString) { + for (let i in root.validators) { + const validator = root.validators[i] + if (!validator.validate(connectionString)) { + d.errorMessage = validator.errorMessage + return + } + d.errorMessage = "" + root.connectionStringFound(connectionString) + } + } + } + + StatusQrCodeScanner { + id: scanner + anchors.horizontalCenter: parent.horizontalCenter + width: 330 + height: 330 + onLastTagChanged: { + d.validateConnectionString(lastTag) + } + } + + Item { + width: parent.width + height: 16 + } StatusBaseText { - id: text - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: 15 + width: parent.width + opacity: scanner.currentTag ? 1 : 0 + wrapMode: Text.WordWrap color: Theme.palette.dangerColor1 - text: qsTr("QR code scanning is not available yet") + horizontalAlignment: Text.AlignHCenter + text: d.errorMessage + } + + StatusBaseText { + width: parent.width + opacity: scanner.camera ? 1 : 0 + wrapMode: Text.WordWrap + color: Theme.palette.baseColor1 + horizontalAlignment: Text.AlignHCenter + text: qsTr("Ensure that the QR code is in focus to scan") } } diff --git a/ui/imports/shared/views/DeviceSyncingView.qml b/ui/imports/shared/views/DeviceSyncingView.qml index aeb6984e07..b07e99f825 100644 --- a/ui/imports/shared/views/DeviceSyncingView.qml +++ b/ui/imports/shared/views/DeviceSyncingView.qml @@ -66,6 +66,7 @@ Item { active: root.userPublicKey == "" Layout.alignment: Qt.AlignHCenter sourceComponent: UserImage { + opacity: name ? 1 : 0 name: root.userDisplayName colorId: root.userColorId colorHash: root.userColorHash @@ -73,6 +74,10 @@ Item { interactive: false imageWidth: 80 imageHeight: 80 + + Behavior on opacity { + NumberAnimation { duration: 250 } + } } } diff --git a/vendor/qzxing b/vendor/qzxing index 80bb1d2190..3fcdd5b65b 160000 --- a/vendor/qzxing +++ b/vendor/qzxing @@ -1 +1 @@ -Subproject commit 80bb1d21903881e4061a41739d413a296ceb3b49 +Subproject commit 3fcdd5b65b9a3dd8bb343cf58743ed1d551e3a92