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:
parent
f0d57d19cf
commit
14c264e350
42
Makefile
42
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:
|
||||
|
|
|
@ -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()}"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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": %* {}
|
||||
}
|
||||
|
|
|
@ -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) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -7,44 +7,114 @@ 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
|
||||
}
|
||||
|
||||
Camera {
|
||||
id: camera
|
||||
captureMode: Camera.CaptureVideo
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
active: false
|
||||
visible: status == Loader.Ready
|
||||
sourceComponent: Camera {
|
||||
focus {
|
||||
focusMode: CameraFocus.FocusContinuous
|
||||
focusPointMode: CameraFocus.FocusPointAuto
|
||||
}
|
||||
}
|
||||
onLoaded: {
|
||||
d.onCameraLoaded()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
|
@ -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 : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ Column {
|
|||
|
||||
StatusSyncCodeInput {
|
||||
id: codeInput
|
||||
implicitWidth: 400
|
||||
width: parent.width
|
||||
mode: StatusSyncCodeInput.WriteMode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 80bb1d21903881e4061a41739d413a296ceb3b49
|
||||
Subproject commit 3fcdd5b65b9a3dd8bb343cf58743ed1d551e3a92
|
Loading…
Reference in New Issue