Implement Qr-code generation and scanning

This commit is contained in:
Dorian Zedler 2021-06-20 15:09:53 +02:00
parent 8d1f0173ed
commit 06e76bc287
Signed by: dorian
GPG key ID: 989DE36109AFA354
13 changed files with 400 additions and 63 deletions

View file

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

View file

@ -33,6 +33,7 @@
#include <QTextCursor>
#include <QPageSize>
#include <QPdfWriter>
#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);
};

View file

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

View file

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

View file

@ -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: "<font size=\"+4\">" + modelData[0] + "</font><br><br> " + 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: "<font size=\"+4\">" + modelData[0] + "</font><br><br> " + 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()
}
}

View file

@ -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 <a href='https://www.gnu.org/licenses/agpl-3.0.en.html'>GNU agplV3 license</a>," +
"the source code can be found <a href='https://itsblue.dev/dorian/blueROCK/'>here</a>.<br><br>" +
"Resultservice and rankings provided by <a href='http://www.digitalROCK.de'>digital ROCK</a>."
}
QrCodeScanPopup {
id: qrCodeScanPopup
Material.theme: root.Material.theme
}
}

View file

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

View file

@ -69,7 +69,7 @@ DataListView {
ToolButton {
id: shareToolBt
onClicked: shareWidget()
onClicked: shareWidget(control.title)
text: "\uf1e0"
font.family: fa5solid.name

View file

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

View file

@ -29,5 +29,6 @@
<file>Components/ColoredItemDelegate.qml</file>
<file>Components/AlignedButton.qml</file>
<file>Components/SharePopup.qml</file>
<file>Components/QrCodeScanPopup.qml</file>
</qresource>
</RCC>

Binary file not shown.

View file

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

View file

@ -22,6 +22,7 @@
#include <QQmlApplicationEngine>
#include <QStyleFactory>
#include <QUrl>
#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")));