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

View File

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

View File

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

View File

@ -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": %* {}
}

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 {
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
}
}
}
}

View File

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

View File

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

View File

@ -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

View File

@ -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()
}
}

View File

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

View File

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

View File

@ -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 }
}
}
}

2
vendor/qzxing vendored

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