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)"
|
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:
|
||||||
|
|
|
@ -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()}"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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": %* {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 : ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ Column {
|
||||||
|
|
||||||
StatusSyncCodeInput {
|
StatusSyncCodeInput {
|
||||||
id: codeInput
|
id: codeInput
|
||||||
implicitWidth: 400
|
width: parent.width
|
||||||
mode: StatusSyncCodeInput.WriteMode
|
mode: StatusSyncCodeInput.WriteMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 80bb1d21903881e4061a41739d413a296ceb3b49
|
Subproject commit 3fcdd5b65b9a3dd8bb343cf58743ed1d551e3a92
|
Loading…
Reference in New Issue