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
This commit is contained in:
Igor Sirotin 2023-03-30 20:57:18 +03:00 committed by GitHub
parent f0d57d19cf
commit 14c264e350
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 484 additions and 172 deletions

View File

@ -181,26 +181,31 @@ ifneq ($(detected_OS),Windows)
NIM_PARAMS += --passL:"-L$(QT5_LIBDIR)" NIM_PARAMS += --passL:"-L$(QT5_LIBDIR)"
endif endif
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 := vendor/DOtherSide/build/lib/libDOtherSideStatic.a
DOTHERSIDE_CMAKE_PARAMS += -DENABLE_DYNAMIC_LIBS=OFF -DENABLE_STATIC_LIBS=ON 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" # 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 else
ifneq ($(QML_DEBUG), false) ifneq ($(QML_DEBUG), false)
QZXING := vendor/DOtherSide/build/qzxing/Debug/qzxing.lib
DOTHERSIDE := vendor/DOtherSide/build/lib/Debug/DOtherSide.dll DOTHERSIDE := vendor/DOtherSide/build/lib/Debug/DOtherSide.dll
QZXING := vendor/DOtherSide/build/qzxing/Debug/qzxing.dll
else else
QZXING := vendor/DOtherSide/build/qzxing/Release/qzxing.lib
DOTHERSIDE := vendor/DOtherSide/build/lib/Release/DOtherSide.dll DOTHERSIDE := vendor/DOtherSide/build/lib/Release/DOtherSide.dll
QZXING := vendor/DOtherSide/build/qzxing/Release/qzxing.dll
endif 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" NIM_EXTRA_PARAMS := --passL:"-lsetupapi -lhid"
endif endif
QZXING_LIBDIR := $(shell pwd)/$(shell dirname "$(QZXING)")
export QZXING_LIBDIR
NIM_PARAMS += --passL:"-L$(QZXING_LIBDIR)" --passL:"-lqzxing"
ifeq ($(detected_OS),Darwin) ifeq ($(detected_OS),Darwin)
ifeq ("$(shell sysctl -nq hw.optional.arm64)","1") ifeq ("$(shell sysctl -nq hw.optional.arm64)","1")
@ -251,11 +256,7 @@ $(DOTHERSIDE): | deps
cd build && \ cd build && \
rm -f CMakeCache.txt && \ rm -f CMakeCache.txt && \
cmake $(DOTHERSIDE_CMAKE_PARAMS)\ cmake $(DOTHERSIDE_CMAKE_PARAMS)\
-DENABLE_DOCS=OFF \ -DENABLE_DOCS=OFF -DENABLE_TESTS=OFF -DQZXING_USE_QML=ON -DQZXING_MULTIMEDIA=ON -DQZXING_USE_DECODER_QR_CODE=ON \
-DENABLE_TESTS=OFF \
-DQZXING_USE_QML=ON \
-DQZXING_MULTIMEDIA=ON \
-DQZXING_USE_DECODER_QR_CODE=ON \
.. $(HANDLE_OUTPUT) && \ .. $(HANDLE_OUTPUT) && \
$(DOTHERSIDE_BUILD_CMD) $(DOTHERSIDE_BUILD_CMD)
@ -400,6 +401,10 @@ $(NIM_STATUS_CLIENT): $(NIM_SOURCES) $(DOTHERSIDE) | check-qt-dir $(STATUSGO) $(
libstatus.dylib \ libstatus.dylib \
@rpath/libstatus.dylib \ @rpath/libstatus.dylib \
bin/nim_status_client && \ bin/nim_status_client && \
install_name_tool -change \
$(QZXING) \
@rpath/libqzxing.dylib \
bin/nim_status_client && \
install_name_tool -change \ install_name_tool -change \
libkeycard.dylib \ libkeycard.dylib \
@rpath/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 tmp/linux/dist/usr/lib/
cp vendor/status-go/build/bin/libstatus.so.0 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 $(STATUSKEYCARDGO) tmp/linux/dist/usr/lib/
cp $(QZXING) tmp/linux/dist/usr/lib/
echo -e $(BUILD_MSG) "AppImage" 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 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 cp bin/nim_windows_launcher.exe $(OUTPUT)/Status.exe
rcedit $(OUTPUT)/bin/Status.exe --set-icon $(OUTPUT)/resources/status.ico rcedit $(OUTPUT)/bin/Status.exe --set-icon $(OUTPUT)/resources/status.ico
rcedit $(OUTPUT)/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 libgcc_s_seh-1.dll)" $(OUTPUT)/bin/
cp "$(shell which libwinpthread-1.dll)" $(OUTPUT)/bin/ cp "$(shell which libwinpthread-1.dll)" $(OUTPUT)/bin/
echo -e $(BUILD_MSG) "deployable folder" 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 \ windeployqt --compiler-runtime --qmldir ui --release \
tmp/windows/dist/Status/bin/DOtherSide.dll tmp/windows/dist/Status/bin/DOtherSide.dll
mv tmp/windows/dist/Status/bin/vc_redist.x64.exe tmp/windows/dist/Status/vendor/ 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 run-linux: nim_status_client
echo -e "\033[92mRunning:\033[39m bin/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 ./bin/nim_status_client
run-macos: 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) run-windows: nim_status_client $(NIM_WINDOWS_PREBUILT_DLLS)
echo -e "\033[92mRunning:\033[39m bin/nim_status_client.exe" 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 ./bin/nim_status_client.exe
tests-nim-linux: | $(DOTHERSIDE) 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 $(ENV_SCRIPT) nim c $(NIM_PARAMS) $(NIM_EXTRA_PARAMS) -r test/nim/message_model_test.nim
statusq-sanity-checker: statusq-sanity-checker:

View File

@ -54,7 +54,7 @@ pipeline {
QTDIR = '/opt/qt/5.15.2/gcc_64' QTDIR = '/opt/qt/5.15.2/gcc_64'
PATH = "${env.QTDIR}/bin:${env.PATH}" PATH = "${env.QTDIR}/bin:${env.PATH}"
/* Include library in order to compile the project */ /* 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 */ /* Container ports */
RPC_PORT = "${8545 + env.EXECUTOR_NUMBER.toInteger()}" RPC_PORT = "${8545 + env.EXECUTOR_NUMBER.toInteger()}"
P2P_PORT = "${6010 + env.EXECUTOR_NUMBER.toInteger()}" P2P_PORT = "${6010 + env.EXECUTOR_NUMBER.toInteger()}"

View File

@ -17,6 +17,7 @@ if defined(macosx):
switch("passL", "-rpath" & " " & getEnv("QT5_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("QT5_LIBDIR"))
switch("passL", "-rpath" & " " & getEnv("STATUSGO_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("STATUSGO_LIBDIR"))
switch("passL", "-rpath" & " " & getEnv("STATUSKEYCARDGO_LIBDIR")) switch("passL", "-rpath" & " " & getEnv("STATUSKEYCARDGO_LIBDIR"))
switch("passL", "-rpath" & " " & getEnv("QZXING_LIBDIR"))
# statically link these libs # statically link these libs
switch("passL", "bottles/openssl@1.1/lib/libcrypto.a") switch("passL", "bottles/openssl@1.1/lib/libcrypto.a")
switch("passL", "bottles/openssl@1.1/lib/libssl.a") switch("passL", "bottles/openssl@1.1/lib/libssl.a")

View File

@ -74,6 +74,11 @@ QtObject:
result.accountsService = accountsService result.accountsService = accountsService
result.localPairingStatus = newLocalPairingStatus() 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) = proc doConnect(self: Service) =
self.events.on(SignalType.Message.event) do(e:Args): self.events.on(SignalType.Message.event) do(e:Args):
let receivedData = MessageSignal(e) let receivedData = MessageSignal(e)
@ -89,10 +94,7 @@ QtObject:
action: signalData.action.parse(), action: signalData.action.parse(),
account: signalData.account, account: signalData.account,
error: signalData.error) error: signalData.error)
self.events.emit(SIGNAL_LOCAL_PAIRING_EVENT, data) self.updateLocalPairingStatus(data)
self.localPairingStatus.update(data.eventType, data.action, data.account, data.error)
self.events.emit(SIGNAL_LOCAL_PAIRING_STATUS_UPDATE, self.localPairingStatus)
proc init*(self: Service) = proc init*(self: Service) =
self.doConnect() self.doConnect()
@ -157,8 +159,18 @@ QtObject:
# Local Pairing # Local Pairing
# #
proc inputConnectionStringForBootstrappingFinished(self: Service, result: string) = proc inputConnectionStringForBootstrappingFinished*(self: Service, responseJson: string) {.slot.} =
discard 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 = proc validateConnectionString*(self: Service, connectionString: string): string =
return status_go.validateConnectionString(connectionString) return status_go.validateConnectionString(connectionString)
@ -187,7 +199,7 @@ QtObject:
"deviceType" : hostOs, "deviceType" : hostOs,
"nodeConfig": nodeConfigJson, "nodeConfig": nodeConfigJson,
"kdfIterations": self.accountsService.getKdfIterations(), "kdfIterations": self.accountsService.getKdfIterations(),
"settingCurrentNetwork": "" "settingCurrentNetwork": "mainnet_rpc"
}, },
"clientConfig": %* {} "clientConfig": %* {}
} }

View File

@ -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) : ""
}
}
}
}

View File

@ -546,44 +546,8 @@ StatusWindow {
Component { Component {
id: qrScannerComponent id: qrScannerComponent
Rectangle { QrCodeScannerPage {
anchors.fill: parent 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
}
}
} }
} }

View File

@ -79,5 +79,6 @@
<file>pages/StatusComboBoxPage.qml</file> <file>pages/StatusComboBoxPage.qml</file>
<file>pages/StatusChartPanelPage.qml</file> <file>pages/StatusChartPanelPage.qml</file>
<file>pages/StatusTextAreaPage.qml</file> <file>pages/StatusTextAreaPage.qml</file>
<file>QrCodeScannerPage.qml</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -7,44 +7,114 @@ import QtGraphicalEffects 1.0
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Backpressure 1.0
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import QZXing 3.3 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 { Item {
id: root 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 sourceSize: Qt.size(videoOutput.sourceRect.width, videoOutput.sourceRect.height)
readonly property size contentSize: Qt.size(videoOutput.contentRect.width, videoOutput.contentRect.height) readonly property size contentSize: Qt.size(videoOutput.contentRect.width, videoOutput.contentRect.height)
readonly property real sourceRatio: videoOutput.sourceRect.width / videoOutput.sourceRect.height readonly property real sourceRatio: videoOutput.sourceRect.width / videoOutput.sourceRect.height
property int failsCount: 0 readonly property int failsCount: d.failsCount
property int tagsCount: 0 readonly property int tagsCount: d.tagsCount
property int decodeTime: 0 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 implicitWidth: sourceSize.width
implicitHeight: sourceSize.height implicitHeight: sourceSize.height
signal tagFound(string tag)
QtObject { QtObject {
id: d id: d
readonly property int radius: 16 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
} }
Camera { property QtObject camera: null
id: camera
captureMode: Camera.CaptureVideo // 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
}
Loader {
id: loader
active: false
visible: status == Loader.Ready
sourceComponent: Camera {
focus { focus {
focusMode: CameraFocus.FocusContinuous focusMode: CameraFocus.FocusContinuous
focusPointMode: CameraFocus.FocusPointAuto focusPointMode: CameraFocus.FocusPointAuto
} }
} }
onLoaded: {
d.onCameraLoaded()
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@ -56,15 +126,16 @@ Item {
anchors.fill: parent anchors.fill: parent
implicitWidth: videoOutput.contentRect.width implicitWidth: videoOutput.contentRect.width
implicitHeight: videoOutput.contentRect.height implicitHeight: videoOutput.contentRect.height
visible: camera.availability === Camera.Available visible: d.camera && d.camera.availability === Camera.Available
clip: true
VideoOutput { VideoOutput {
id: videoOutput id: videoOutput
anchors.fill: parent anchors.fill: parent
visible: false visible: false
source: camera source: d.camera
filters: [ qzxingFilter ] filters: [ qzxingFilter ]
fillMode: VideoOutput.PreserveAspectFit fillMode: VideoOutput.PreserveAspectCrop
} }
Rectangle { Rectangle {
@ -81,83 +152,138 @@ Item {
maskSource: mask 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 { QZXingFilter {
id: qzxingFilter id: qzxingFilter
orientation: videoOutput.orientation orientation: videoOutput.orientation
captureRect: { captureRect: {
videoOutput.contentRect; videoOutput.sourceRect; // bindings videoOutput.contentRect; videoOutput.sourceRect // bindings
const normalizedRectangle = Qt.rect(0, 0, 1, 1) const normalizedRectangle = root.captureRectangle
const rectangle = videoOutput.mapNormalizedRectToItem(normalizedRectangle) const rectangle = videoOutput.mapNormalizedRectToItem(normalizedRectangle)
return videoOutput.mapRectToSource(rectangle); return videoOutput.mapRectToSource(rectangle);
} }
decoder { decoder {
enabledDecoders: QZXing.DecoderFormat_QR_CODE enabledDecoders: QZXing.DecoderFormat_QR_CODE
tryHarder: true
onTagFound: { onTagFound: {
root.lastTag = tag d.currentTag = tag
d.lastTag = tag
root.tagFound(tag) root.tagFound(tag)
} }
} }
onDecodingFinished: { onDecodingFinished: {
if (succeeded) if (succeeded) {
++root.tagsCount ++d.tagsCount
else } else {
++root.failsCount ++d.failsCount
root.decodeTime = decodeTime d.currentTag = ""
}
d.decodeTime = decodeTime
} }
} }
} }
// TODO: Implement camera selector ColumnLayout {
// For me it works once. The first switch between 2 cameras is ok. anchors.fill: parent
// The second switch doesn't work and behaves different with 2 approaches: visible: loader.status !== Loader.Ready || loader.status === Loader.Error
// With standard `ComboBox` it throws an exception. spacing: 20
// Width `StatusComboBox` it just kinda unbinds from the Camera.
//
// ComboBox {
// id: cameraComboBox
// anchors { Item {
// right: parent.right Layout.fillHeight: true
// bottom: parent.bottom Layout.fillWidth: true
// margins: 10 }
// }
// width: implicitWidth StatusBaseText {
// opacity: 0.7 Layout.fillWidth: true
// model: QtMultimedia.availableCameras text: qsTr('Enable access to your camera')
// textRole: "displayName" leftPadding: 48
// valueRole: "deviceId" rightPadding: 48
// onCurrentValueChanged: { font.pixelSize: 15
// camera.deviceId = currentValue wrapMode: Text.WordWrap
// } horizontalAlignment: Text.AlignHCenter
// } }
// StatusComboBox { StatusBaseText {
// id: cameraComboBox Layout.fillWidth: true
// anchors { text: qsTr("To scan a QR, Status needs\naccess to your webcam")
// right: parent.right leftPadding: 48
// bottom: parent.bottom rightPadding: 48
// margins: 10 font.pixelSize: 15
// } wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
color: Theme.palette.directColor4
}
// width: implicitWidth StatusButton {
// opacity: 0.7 id: button
// model: QtMultimedia.availableCameras Layout.alignment: Qt.AlignHCenter
// control.textRole: "displayName" text: qsTr("Enable camera access")
// control.valueRole: "deviceId" onClicked: {
// onCurrentValueChanged: { loading = true
// console.log("setting deviceId to", currentValue) Backpressure.debounce(this, 250, () => { loader.active = true })()
// camera.deviceId = currentValue }
// } }
// }
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 { ColumnLayout {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width width: parent.width
spacing: 10 spacing: 10
StatusBaseText { StatusBaseText {
@ -165,7 +291,7 @@ Item {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Theme.palette.dangerColor1 color: Theme.palette.dangerColor1
visible: camera.availability !== Camera.Available visible: d.camera && d.camera.availability !== Camera.Available
text: qsTr("Camera is not available") text: qsTr("Camera is not available")
} }
@ -174,8 +300,8 @@ Item {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: Theme.palette.directColor5 color: Theme.palette.directColor5
visible: camera.errorCode !== Camera.NoError visible: d.camera && d.camera.errorCode !== Camera.NoError
text: "Error comes here" // camera.errorString text: d.camera ? d.camera.errorString : ""
} }
} }
} }

View File

@ -61,9 +61,6 @@ Item {
ComboBox { ComboBox {
id: comboBox id: comboBox
property color bgColor: Theme.palette.baseColor2
property color bgColorHover: bgColor
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
Layout.topMargin: labelItem.visible ? 7 : 0 Layout.topMargin: labelItem.visible ? 7 : 0
@ -77,7 +74,7 @@ Item {
spacing: 16 spacing: 16
background: Rectangle { background: Rectangle {
implicitHeight: 56 implicitHeight: 24 + comboBox.topPadding + comboBox.bottomPadding
implicitWidth: 448 implicitWidth: 448
color: root.type === StatusComboBox.Type.Secondary ? "transparent" : Theme.palette.baseColor2 color: root.type === StatusComboBox.Type.Secondary ? "transparent" : Theme.palette.baseColor2
radius: 8 radius: 8

View File

@ -34,21 +34,26 @@ Item {
QtObject { QtObject {
id: d id: d
property string connectionString
function validateConnectionString(connectionString) { function validateConnectionString(connectionString) {
const result = root.startupStore.validateLocalPairingConnectionString(connectionString) const result = root.startupStore.validateLocalPairingConnectionString(connectionString)
return result === "" return result === ""
} }
function onConnectionStringFound(connectionString) {
root.startupStore.setConnectionString(connectionString)
root.startupStore.doPrimaryAction()
}
} }
Timer { Timer {
id: nextStateDelay id: nextStateDelay
property string connectionString
interval: 1000 interval: 1000
repeat: false repeat: false
onTriggered: { onTriggered: {
root.startupStore.setConnectionString(d.connectionString) d.onConnectionStringFound(connectionString)
root.startupStore.doPrimaryAction()
} }
} }
@ -84,13 +89,10 @@ Item {
} }
StackLayout { StackLayout {
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
implicitWidth: Math.max(mobileSync.implicitWidth, desktopSync.implicitWidth) height: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight)
implicitHeight: Math.max(mobileSync.implicitHeight, desktopSync.implicitHeight)
currentIndex: switchTabBar.currentIndex currentIndex: switchTabBar.currentIndex
clip: true
// StackLayout doesn't support alignment, so we create an `Item` wrappers // StackLayout doesn't support alignment, so we create an `Item` wrappers
@ -98,17 +100,22 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
implicitWidth: mobileSync.implicitWidth
implicitHeight: mobileSync.implicitHeight
SyncDeviceFromMobile { SyncDeviceFromMobile {
id: mobileSync id: mobileSync
anchors { anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter 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: { onConnectionStringFound: {
d.processConnectionString(connectionString) d.onConnectionStringFound(connectionString)
} }
} }
} }
@ -117,9 +124,6 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
implicitWidth: desktopSync.implicitWidth
implicitHeight: desktopSync.implicitHeight
SyncDeviceFromDesktop { SyncDeviceFromDesktop {
id: desktopSync id: desktopSync
anchors { anchors {
@ -130,19 +134,15 @@ Item {
input.readOnly: nextStateDelay.running input.readOnly: nextStateDelay.running
input.validators: [ input.validators: [
StatusValidator { StatusValidator {
name: "asyncConnectionString" name: "isSyncCode"
errorMessage: qsTr("This does not look like a sync code") errorMessage: qsTr("This does not look like a sync code")
validate: (value) => { validate: d.validateConnectionString
return d.validateConnectionString(value)
}
} }
] ]
input.onValidChanged: { input.onValidChanged: {
if (!input.valid) { if (!input.valid)
d.connectionString = ""
return return
} nextStateDelay.connectionString = desktopSync.input.text
d.connectionString = desktopSync.input.text
nextStateDelay.start() nextStateDelay.start()
} }
} }

View File

@ -10,7 +10,7 @@ Column {
StatusSyncCodeInput { StatusSyncCodeInput {
id: codeInput id: codeInput
implicitWidth: 400 width: parent.width
mode: StatusSyncCodeInput.WriteMode mode: StatusSyncCodeInput.WriteMode
} }
} }

View File

@ -3,29 +3,70 @@ import QtQuick.Layouts 1.12
import QtQuick.Controls 2.13 import QtQuick.Controls 2.13
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Controls.Validators 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
Rectangle { Column {
id: root id: root
property list<StatusValidator> validators
signal connectionStringFound(connectionString: string) signal connectionStringFound(connectionString: string)
implicitWidth: 330 spacing: 12
implicitHeight: 330
radius: 16 QtObject {
color: Theme.palette.baseColor4 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 { StatusBaseText {
id: text width: parent.width
anchors.fill: parent opacity: scanner.currentTag ? 1 : 0
horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
font.pixelSize: 15
color: Theme.palette.dangerColor1 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")
} }
} }

View File

@ -66,6 +66,7 @@ Item {
active: root.userPublicKey == "" active: root.userPublicKey == ""
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
sourceComponent: UserImage { sourceComponent: UserImage {
opacity: name ? 1 : 0
name: root.userDisplayName name: root.userDisplayName
colorId: root.userColorId colorId: root.userColorId
colorHash: root.userColorHash colorHash: root.userColorHash
@ -73,6 +74,10 @@ Item {
interactive: false interactive: false
imageWidth: 80 imageWidth: 80
imageHeight: 80 imageHeight: 80
Behavior on opacity {
NumberAnimation { duration: 250 }
}
} }
} }

2
vendor/qzxing vendored

@ -1 +1 @@
Subproject commit 80bb1d21903881e4061a41739d413a296ceb3b49 Subproject commit 3fcdd5b65b9a3dd8bb343cf58743ed1d551e3a92