diff --git a/blueROCK.pro b/blueROCK.pro index a5c666b..718c049 100644 --- a/blueROCK.pro +++ b/blueROCK.pro @@ -98,6 +98,12 @@ ios { xcode_product_bundle_identifier_setting.value = "de.itsblue.bluerock" } +CONFIG += enable_decoder_qr_code \ + enable_encoder_qr_code \ + qzxing_multimedia \ + qzxing_qml +include(qzxing/src/QZXing-components.pri) + # this has to be the last line! ANDROID_ABIS = armeabi-v7a arm64-v8a diff --git a/headers/bluerockbackend.h b/headers/bluerockbackend.h index b03b146..0819bb9 100644 --- a/headers/bluerockbackend.h +++ b/headers/bluerockbackend.h @@ -33,6 +33,7 @@ #include #include #include +#include "QZXing.h" #include "shareUtils/shareutils.h" @@ -43,9 +44,10 @@ public: explicit BlueRockBackend(QObject *parent = nullptr); private: - QVariantMap senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); + QVariantMap _senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); ShareUtils* _shareUtils; + const QStringList _validBaseDomains = {"digitalrock.de", "bluerock.dev"}; signals: @@ -53,8 +55,8 @@ public slots: QVariant getWidgetData(QVariantMap params); QVariantMap getParamsFromUrl(QString url); - void shareResultsAsUrl(QString url); - void shareResultsAsPoster(QString url); + void shareResultsAsUrl(QString url, QString compName); + void shareResultsAsPoster(QString url, QString compName); }; diff --git a/resources/qml/Components/QrCodeScanPopup.qml b/resources/qml/Components/QrCodeScanPopup.qml new file mode 100644 index 0000000..a02f341 --- /dev/null +++ b/resources/qml/Components/QrCodeScanPopup.qml @@ -0,0 +1,203 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QZXing 3.1 +import QtMultimedia 5.12 +import QtQuick.Shapes 1.12 +import QtQuick.Controls.Material 2.12 + + +Dialog { + id: control + + property string _statusText: "" + property string _statusColor: Material.primaryTextColor + property bool _freezeScanning: false + + parent: Overlay.overlay + + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + height: app.height * 0.8 + width: app.width * 0.8 + + modal: true + title: "Scan QR-Code" + + standardButtons: Dialog.Cancel + + onOpened: { + setDefaultStatusText() + control._freezeScanning = false + cameraLoader.sourceComponent = cameraComponent + } + + onClosed: cameraLoader.sourceComponent = null + + function setDefaultStatusText() { + _statusText = "Place the Code in the center" + _statusColor = Material.primaryTextColor + } + + contentItem: Loader { + id: cameraLoader + + asynchronous: true + sourceComponent: null + } + + Component { + id: cameraComponent + Item { + anchors.fill: parent + + Camera { + id: camera + captureMode: Camera.CaptureStillImage + imageProcessing.whiteBalanceMode: CameraImageProcessing.WhiteBalanceAuto + + focus { + focusMode: Camera.FocusContinuous + focusPointMode: Camera.FocusPointCenter + } + } + + VideoOutput { + id: videoOutput + x: 0 + y: 0 + width: parent.width + height: parent.height + + fillMode: VideoOutput.PreserveAspectCrop + + source: camera + filters: [ zxingFilter ] + focus : visible // to receive focus and capture key events when visible + + autoOrientation: true + + MouseArea { + anchors.fill: parent + + onClicked: { + if (camera.lockStatus !== Camera.Unlocked) + camera.unlock(); + + camera.searchAndLock(); + } + } + + Rectangle { + anchors { + top: parent.top + left: parent.left + right: app.landscape() ? focusIndicatorRect.left : parent.right + bottom: app.landscape() ? parent.bottom : focusIndicatorRect.top + } + + opacity: focusIndicatorRect.opacity + color: focusIndicatorRect.border.color + } + + Rectangle { + id: focusIndicatorRect + anchors.centerIn: parent + + width: Math.min(parent.height, parent.width) + height: width + + border.width: width * 0.1 + border.color: "#000000" + + opacity: 0.3 + color: "transparent" + } + + Rectangle { + anchors { + bottom: focusIndicatorRect.bottom + bottomMargin: height * 0.5 + horizontalCenter: focusIndicatorRect.horizontalCenter + } + + width: (focusIndicatorRect.width - focusIndicatorRect.border.width * 2) * 0.8 + height: focusIndicatorRect.border.width + + radius: height * 0.3 + + color: Material.backgroundColor + + Material.elevation: 10 + + + Label { + anchors { + fill: parent + margins: height * 0.1 + } + + color: control._statusColor + + font.pixelSize: height * 0.5 + fontSizeMode: Text.Fit + minimumPixelSize: height * 0.2 + + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + + text: control._statusText + } + } + + Rectangle { + anchors { + top: app.landscape() ? parent.top : focusIndicatorRect.bottom + left: app.landscape() ? focusIndicatorRect.right : parent.left + right: parent.right + bottom: parent.bottom + } + + opacity: focusIndicatorRect.opacity + color: focusIndicatorRect.border.color + } + } + } + } + + + QZXingFilter { + id: zxingFilter + + decoder { + onTagFound: { + if(control._freezeScanning) + return + + control._freezeScanning = true + + control._statusText = "Plase wait..." + + if(app.openWidgetFromUrl(tag)) + control.close() + else { + control._statusText = "Invalid QR-Code" + control._statusColor = Material.color(Material.Red) + statusTextResetTimer.start() + control._freezeScanning = false + } + } + + enabledDecoders: QZXing.DecoderFormat_QR_CODE + } + } + + Timer { + id: statusTextResetTimer + running: false + repeat: false + interval: 3000 + onTriggered: setDefaultStatusText() + } +} diff --git a/resources/qml/Components/ResultDelegate.qml b/resources/qml/Components/ResultDelegate.qml index 790e083..dd8125b 100644 --- a/resources/qml/Components/ResultDelegate.qml +++ b/resources/qml/Components/ResultDelegate.qml @@ -226,7 +226,7 @@ ColoredItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); if(resultData[1] > 0){ @@ -254,7 +254,7 @@ ColoredItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); @@ -279,7 +279,7 @@ ColoredItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); } } diff --git a/resources/qml/Components/SharePopup.qml b/resources/qml/Components/SharePopup.qml index be10465..d566d02 100644 --- a/resources/qml/Components/SharePopup.qml +++ b/resources/qml/Components/SharePopup.qml @@ -1,11 +1,13 @@ import QtQuick 2.0 import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 +import QZXing 3.1 Dialog { id: control property string _shareUrl + property string _compName parent: Overlay.overlay @@ -13,31 +15,78 @@ Dialog { y: (parent.height - height) * 0.5 modal: true - title: "Share these results" - contentItem: RowLayout { - Repeater { - id: buttonRepeater - property var buttons: [ - ["\uf0c1", "Link", serverConn.shareResultsAsUrl], - ["\uf029", "QR-code", null], - ["\uf1c1", "Poster", serverConn.shareResultsAsPoster], - ] - - model: buttons - - delegate: Button { - flat: true - font.family: fa5solid.name - text: "" + modelData[0] + "

" + modelData[1] + " " - onClicked: buttonRepeater.buttons[index][2](_shareUrl) - } - } + onClosed: { + shareComponentLoader.sourceComponent = null } - function appear(shareUrl) { + contentItem: Loader { + id: shareComponentLoader + + asynchronous: false + sourceComponent: null + } + + Component { + id: shareComponent + StackLayout { + id: stackLayout + currentIndex: 0 + + RowLayout { + id: menuRow + Repeater { + id: buttonRepeater + property var buttons: [ + ["\uf0c1", "Link", serverConn.shareResultsAsUrl], + ["\uf029", "QR-code", function() { + stackLayout.currentIndex = 1 + } + ], + ["\uf1c1", "Poster", serverConn.shareResultsAsPoster], + ] + + model: buttons + + delegate: Button { + flat: true + font.family: fa5solid.name + text: "" + modelData[0] + "

" + modelData[1] + " " + onClicked: buttonRepeater.buttons[index][2](_shareUrl, _compName) + } + } + } + + + Image { + id: qrCodeImage + + property int size: stackLayout.currentIndex === 1 ? (app.landscape() ? app.height * 0.8 : app.width * 0.8):menuRow.height + + Layout.preferredHeight: size + Layout.preferredWidth: size + + sourceSize.width: size + sourceSize.height: size + + fillMode: Image.PreserveAspectFit + + source: "image://QZXing/encode/" + _shareUrl + "?border=true&correctionLevel=H" + + Behavior on size { + NumberAnimation { + duration: 200 + } + } + } + } + } + + function appear(shareUrl, compName) { _shareUrl = shareUrl + _compName = compName + shareComponentLoader.sourceComponent = shareComponent control.open() } } diff --git a/resources/qml/Pages/StartPage.qml b/resources/qml/Pages/StartPage.qml index 5878095..7734679 100644 --- a/resources/qml/Pages/StartPage.qml +++ b/resources/qml/Pages/StartPage.qml @@ -38,7 +38,7 @@ Page { topMargin: root.height * 0.03 } - height: menuGr.buttonSize * 0.3 + height: app.landscape() ? menuGr.buttonSize * 0.2:menuGr.buttonSize * 0.3 } GridLayout { @@ -84,7 +84,7 @@ Page { } } - GridLayout { + Grid { id: footerMenu anchors { @@ -93,6 +93,11 @@ Page { horizontalCenter: parent.horizontalCenter } + width: app.landscape() ? childrenRect.width : parent.width * 0.8 + height: app.landscape() ? headerBadge.height : headerBadge.height * 2 + + columnSpacing: height * 0.1 + columns: app.landscape() ? 4:2 rows: app.landscape() ? 1:2 @@ -102,18 +107,19 @@ Page { ["\uf059", "IFSC results", ifscDisclaimerDialog.open], ["\uf042", Material.theme === Material.Light ? "Dark mode":"Light mode", app.toggleDarkMode], ["\uf05a", "About blueROCK", aboutBluerockDisclaimerDialog.open], - ["\uf029", "Scan QR code", null], + ["\uf029", "Scan QR code", qrCodeScanPopup.open], ] model: buttons delegate: Item { - Layout.preferredWidth: app.landscape() ? footerMenuButton.implicitWidth : root.width * 0.5 - (footerMenu.columnSpacing / 2) - Layout.preferredHeight: footerMenuButton.implicitHeight + width: app.landscape() ? footerMenuButton.implicitWidth : footerMenu.width * 0.5 - (footerMenu.columnSpacing / 2) + height: app.landscape() ? footerMenu.height : footerMenu.height * 0.5 - (footerMenu.rowSpacing / 2) Button { id: footerMenuButton + property bool isLeft: index % 2 === 0 anchors { @@ -122,6 +128,8 @@ Page { centerIn: app.landscape() ? parent : undefined } + height: parent.height + flat: true font.family: fa5solid.name @@ -157,7 +165,11 @@ Page { "This app is open source and licensed under the GNU agplV3 license," + "the source code can be found here.

" + "Resultservice and rankings provided by digital ROCK." + } + QrCodeScanPopup { + id: qrCodeScanPopup + Material.theme: root.Material.theme } } diff --git a/resources/qml/Pages/WidgetPage.qml b/resources/qml/Pages/WidgetPage.qml index cd9ac93..ecaf211 100644 --- a/resources/qml/Pages/WidgetPage.qml +++ b/resources/qml/Pages/WidgetPage.qml @@ -38,7 +38,9 @@ Page { Result, Ranking, - Aggregated // not yet implemented + Aggregated, // not yet implemented + + Invalid } title: widgetLd.item !== null && widgetLd.item.hasOwnProperty('title') ? widgetLd.item['title']:"" @@ -72,7 +74,6 @@ Page { // route: (int) round // type: ('','starters', 'nat_team_ranking', 'sektionenwertung', 'regionalzentren'), //} - var ret = serverConn.getWidgetData(params) root.status = ret["status"] @@ -80,7 +81,11 @@ Page { if(ret["status"] === 200){ root.widgetData = ret["data"] root.widgetType = checkWidgetType(params, root.widgetData) - if(widgetLd.load()){ + if(widgetType === WidgetPage.WidgetType.Invalid) { + root.ready = false + root.status = 906 + } + else if(widgetLd.load()){ root.ready = true } else { @@ -124,6 +129,10 @@ Page { } + function areParamsValid() { + + } + function checkWidgetType(params, widgetData){ var widgetType @@ -170,6 +179,9 @@ Page { // aggregated widgetType = WidgetPage.WidgetType.Aggregated } + else { + widgetType = WidgetPage.WidgetType.Invalid + } return widgetType } @@ -181,10 +193,9 @@ Page { return ret.join('&'); } - function shareWidget() { + function shareWidget(compName) { var url = "https://l.bluerock.dev/?" + encodeQueryData(params) - sharePu.appear(url) - console.log("Url will be:", url) + sharePu.appear(url, compName) } Loader { diff --git a/resources/qml/Widgets/ResultWidget.qml b/resources/qml/Widgets/ResultWidget.qml index 9a07249..418aa5d 100644 --- a/resources/qml/Widgets/ResultWidget.qml +++ b/resources/qml/Widgets/ResultWidget.qml @@ -69,7 +69,7 @@ DataListView { ToolButton { id: shareToolBt - onClicked: shareWidget() + onClicked: shareWidget(control.title) text: "\uf1e0" font.family: fa5solid.name diff --git a/resources/qml/main.qml b/resources/qml/main.qml index b60c655..5f6242d 100644 --- a/resources/qml/main.qml +++ b/resources/qml/main.qml @@ -28,6 +28,8 @@ import de.itsblue.blueROCK 1.0 import "./Pages" import "./Components" +import "./Widgets" + Window { visible: true width: 540 @@ -115,7 +117,7 @@ Window { 'sui_ice' : { 'label' : 'Iceclimbing', 'nation' : 'SUI', - 'wettk_reg' : '^[0-9]{2,2}_RC_.*', + 'wparams["valid"]ettk_reg' : '^[0-9]{2,2}_RC_.*', 'rang_title': '', 'bgcolor' : app.federalColor, //'#F0F0F0', 'sort_rank': 4, @@ -134,6 +136,7 @@ Window { //mainStack.push("Pages/AthleteSearchPage.qml") openWidget({comp: 11651, cat: 26}) //openWidget({person: 6623}) + //console.log(JSON.stringify(serverConn.getParamsFromUrl(""))) } FontLoader { @@ -154,11 +157,6 @@ Window { BlueRockBackend { id: serverConn - - Component.onCompleted: { - //var params = serverConn.getParamsFromUrl("https://www.digitalrock.de/egroupware/ranking/sitemgr/digitalrock/eliste.html#!comp=11471&cat=GER_F_A") - //app.openWidget(params) - } } AppSettings { @@ -539,17 +537,36 @@ Window { function openWidget(params) { loadingDl.open() - var calComp = Qt.createComponent("qrc:/Pages/WidgetPage.qml").createObject(null, {"params": params}) - app.errorCode = calComp.status + console.log("Opening widget: ", JSON.stringify(params)) - if(calComp.ready) { - mainStack.push(calComp) - } - else { - delete(calComp) + var result = false + + if(Object.keys(params).length) { + var calComp = Qt.createComponent("qrc:/Pages/WidgetPage.qml").createObject(null, {"params": params}) + app.errorCode = calComp.status + + if(calComp.ready) { + mainStack.push(calComp) + result = true + } + else { + delete(calComp) + } } loadingDl.close() + return result + } + + function openWidgetFromUrl(url) { + var result = serverConn.getParamsFromUrl(url) + + if(result["valid"]) { + openWidget(result["params"]) + return app.errorCode !== 906 + } + + return result["valid"] } function defaultString(string, defaultString) { @@ -592,6 +609,11 @@ Window { errorString = "Authentication required" errorDescription = "The server asked for user credentinals, please chack them and try again" break + case 404: + infoLevel = 2 + errorString = "Not found" + errorDescription = "The requested item was not found" + break case 500: infoLevel = 2 errorString = "Internal server error" @@ -627,6 +649,11 @@ Window { errorString = "Loading..." errorDescription = "Please wait while we're loading some data" break + case 906: + infoLevel = 2 + errorString = "Invalid Request" + errorDescription = "Invalid Request" + break default: infoLevel = 2 errorString = "Unexpected error ("+errorCode+")" diff --git a/resources/qml/qml.qrc b/resources/qml/qml.qrc index 2d4373e..5fe014c 100644 --- a/resources/qml/qml.qrc +++ b/resources/qml/qml.qrc @@ -29,5 +29,6 @@ Components/ColoredItemDelegate.qml Components/AlignedButton.qml Components/SharePopup.qml + Components/QrCodeScanPopup.qml diff --git a/resources/shared/PosterTemplate.xcf b/resources/shared/PosterTemplate.xcf index 45fbaae..9781e61 100644 Binary files a/resources/shared/PosterTemplate.xcf and b/resources/shared/PosterTemplate.xcf differ diff --git a/sources/bluerockbackend.cpp b/sources/bluerockbackend.cpp index c81f6dd..21ce732 100644 --- a/sources/bluerockbackend.cpp +++ b/sources/bluerockbackend.cpp @@ -21,7 +21,6 @@ BlueRockBackend::BlueRockBackend(QObject *parent) : QObject(parent) { this->_shareUtils = new ShareUtils(this); - this->shareResultsAsPoster("test"); } QVariant BlueRockBackend::getWidgetData(QVariantMap params) { @@ -49,7 +48,7 @@ QVariant BlueRockBackend::getWidgetData(QVariantMap params) { qDebug() << requestUrl; - QVariantMap ret = this->senddata(QUrl(requestUrl)); + QVariantMap ret = this->_senddata(QUrl(requestUrl)); if(ret["status"] != 200) { // request was a failure @@ -67,6 +66,18 @@ QVariant BlueRockBackend::getWidgetData(QVariantMap params) { QVariantMap BlueRockBackend::getParamsFromUrl(QString stringUrl) { stringUrl = stringUrl.replace("#!", "?"); QUrl url(stringUrl); + + if(!url.isValid() || url.isEmpty() || url.host().isEmpty()) + return {{"valid", false},{"params", QVariantMap()}} ; + + QStringList domainFragments = url.host().split("."); + QString tld = domainFragments.takeLast(); + QString domainName = domainFragments.takeLast(); + QString baseDomain = domainName + "." + tld; + + if(!this->_validBaseDomains.contains(baseDomain)) + return {{"valid", false},{"params", QVariantMap()}}; + QUrlQuery query(url.query()); QVariantMap params; @@ -78,31 +89,42 @@ QVariantMap BlueRockBackend::getParamsFromUrl(QString stringUrl) { params.insert(pair.first, pair.second); } - return params; + return {{"valid", true},{"params",params}}; } -void BlueRockBackend::shareResultsAsUrl(QString url) { - this->_shareUtils->shareText(url); +void BlueRockBackend::shareResultsAsUrl(QString url, QString compName) { + this->_shareUtils->shareText("Check out the results of " + compName + " over here:\n" + url); } -void BlueRockBackend::shareResultsAsPoster(QString url) { - QPdfWriter writer("/tmp/test.pdf"); +void BlueRockBackend::shareResultsAsPoster(QString url, QString compName) { + QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + path += "/" + compName + ".pdf"; + QPdfWriter writer(path); writer.setPageSize(QPageSize(QPageSize::A4)); writer.setPageMargins(QMargins(0, 0, 0, 0)); writer.setResolution(600); + QPainter painter(&writer); - painter.drawText(QRect(0, 0, 1980, 100),Qt::AlignHCenter|Qt::AlignBottom, - "Children's Health Checkup Form"); - QPixmap image(":/PosterTemplate.png"); - painter.drawPixmap(0,0, writer.width(), writer.height(), image); + QPixmap background(":/PosterTemplate.png"); + painter.drawPixmap(0,0, writer.width(), writer.height(), background); + + QPixmap barcode; + int size = writer.width() * 0.5; + QZXingEncoderConfig encoderConfig(QZXing::EncoderFormat_QR_CODE, QSize(size, size), QZXing::EncodeErrorCorrectionLevel_M, false, false); + barcode.convertFromImage(QZXing::encodeData(url, encoderConfig)); + painter.drawPixmap((writer.width() - size) / 2, size * 0.5, size, size, barcode); + painter.end(); + + int requestId; + this->_shareUtils->sendFile(path, compName, "application/pdf", requestId); } // ------------------------ // --- Helper functions --- // ------------------------ -QVariantMap BlueRockBackend::senddata(QUrl serviceUrl, QUrlQuery pdata) +QVariantMap BlueRockBackend::_senddata(QUrl serviceUrl, QUrlQuery pdata) { // create network manager QNetworkAccessManager * networkManager = new QNetworkAccessManager(); diff --git a/sources/main.cpp b/sources/main.cpp index b09f017..fc04d7e 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -22,6 +22,7 @@ #include #include #include +#include "QZXing.h" #include "headers/bluerockbackend.h" #include "headers/appsettings.h" @@ -46,6 +47,9 @@ int main(int argc, char *argv[]) engine.rootContext()->setContextProperty("QT_DEBUG", false); #endif + QZXing::registerQMLTypes(); + QZXing::registerQMLImageProvider(engine); + engine.rootContext()->setContextProperty("APP_VERSION", APP_VERSION); engine.load(QUrl(QStringLiteral("qrc:/main.qml")));