Started to implement propper permission handling
This commit is contained in:
parent
9ca38f8651
commit
5a50ef8cbe
7 changed files with 181 additions and 19 deletions
|
@ -35,6 +35,10 @@
|
||||||
#include <QPdfWriter>
|
#include <QPdfWriter>
|
||||||
#include "QZXing.h"
|
#include "QZXing.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
#include <QtAndroidExtras>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "shareUtils/shareutils.h"
|
#include "shareUtils/shareutils.h"
|
||||||
|
|
||||||
class BlueRockBackend : public QObject
|
class BlueRockBackend : public QObject
|
||||||
|
@ -54,10 +58,14 @@ signals:
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
QVariant getWidgetData(QVariantMap params);
|
Q_INVOKABLE QVariant getWidgetData(QVariantMap params);
|
||||||
QVariantMap getParamsFromUrl(QString url);
|
Q_INVOKABLE QVariantMap getParamsFromUrl(QString url);
|
||||||
void shareResultsAsUrl(QString url, QString compName);
|
Q_INVOKABLE void shareResultsAsUrl(QString url, QString compName);
|
||||||
void shareResultsAsPoster(QString url, QString compName);
|
Q_INVOKABLE void shareResultsAsPoster(QString url, QString compName);
|
||||||
|
|
||||||
|
Q_INVOKABLE bool isCameraPermissionGranted();
|
||||||
|
Q_INVOKABLE bool requestCameraPermission();
|
||||||
|
|
||||||
#if defined(Q_OS_ANDROID)
|
#if defined(Q_OS_ANDROID)
|
||||||
void onApplicationStateChanged(Qt::ApplicationState applicationState);
|
void onApplicationStateChanged(Qt::ApplicationState applicationState);
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -31,7 +31,10 @@ Dialog {
|
||||||
onOpened: {
|
onOpened: {
|
||||||
setDefaultStatusText()
|
setDefaultStatusText()
|
||||||
control._freezeScanning = false
|
control._freezeScanning = false
|
||||||
cameraLoader.sourceComponent = cameraComponent
|
if(serverConn.isCameraPermissionGranted())
|
||||||
|
cameraLoader.sourceComponent = cameraComponent
|
||||||
|
else
|
||||||
|
cameraLoader.sourceComponent = noPermissionComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosed: cameraLoader.sourceComponent = null
|
onClosed: cameraLoader.sourceComponent = null
|
||||||
|
@ -168,6 +171,65 @@ Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: noPermissionComponent
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: noPermissionIcon
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
topMargin: parent.height * 0
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
font.pixelSize: parent.height * 0.3
|
||||||
|
font.family: fa5solid.name
|
||||||
|
text: "\uf3ed"
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
id: noPermissionText
|
||||||
|
anchors {
|
||||||
|
top: noPermissionIcon.bottom
|
||||||
|
topMargin: noPermissionIcon.height * 0.15
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent.width * 0.9
|
||||||
|
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: noPermissionIcon.height * 0.15
|
||||||
|
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
|
||||||
|
//% "Camera permission denied!"
|
||||||
|
text: qsTrId("#cameraPermissionDenied")
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: noPermissionDetailText
|
||||||
|
anchors {
|
||||||
|
top: noPermissionText.bottom
|
||||||
|
topMargin: noPermissionText.height * 0.15
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent.width * 0.9
|
||||||
|
|
||||||
|
font.pixelSize: noPermissionText.font.pixelSize * 0.7
|
||||||
|
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
|
||||||
|
//% "This app requires access to your camera in order to scan QR-Codes. It will never record or store any photos or videos."
|
||||||
|
text: qsTrId("#cameraPermissionDeniedDetails")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QZXingFilter {
|
QZXingFilter {
|
||||||
id: zxingFilter
|
id: zxingFilter
|
||||||
|
|
Binary file not shown.
|
@ -5,21 +5,33 @@
|
||||||
<name></name>
|
<name></name>
|
||||||
<message id="#scanQrCode">
|
<message id="#scanQrCode">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="27"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="27"/>
|
||||||
|
<location filename="../qml/Pages/StartPage.qml" line="120"/>
|
||||||
<source>Scan QR-Code</source>
|
<source>Scan QR-Code</source>
|
||||||
<translation>QR-Code scannen</translation>
|
<translation>QR-Code scannen</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#placeQrCodeInCenter">
|
<message id="#placeQrCodeInCenter">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="41"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="44"/>
|
||||||
<source>Place the Code in the center</source>
|
<source>Place the Code in the center</source>
|
||||||
<translation>Positioniere den Code in der Mitte</translation>
|
<translation>Positioniere den Code in der Mitte</translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message id="#cameraPermissionDenied">
|
||||||
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="208"/>
|
||||||
|
<source>Camera permission denied!</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message id="#cameraPermissionDeniedDetails">
|
||||||
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="228"/>
|
||||||
|
<source>This app requires access to your camera in order to scan QR-Codes. It will never record or store any photos or videos.</source>
|
||||||
|
<oldsource>This app requires access to your camera in order to scan QR-Codes. It will never record store any photos or videos.</oldsource>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
<message id="#pleaseWait">
|
<message id="#pleaseWait">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="183"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="245"/>
|
||||||
<source>Plase wait</source>
|
<source>Plase wait</source>
|
||||||
<translation>Bitte warten</translation>
|
<translation>Bitte warten</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#invalidQrCode">
|
<message id="#invalidQrCode">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="189"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="251"/>
|
||||||
<source>Invalid QR-Code</source>
|
<source>Invalid QR-Code</source>
|
||||||
<translation>Ungültiger QR-Code</translation>
|
<translation>Ungültiger QR-Code</translation>
|
||||||
</message>
|
</message>
|
||||||
|
@ -55,9 +67,8 @@
|
||||||
<translation>Jetzt kaufen für</translation>
|
<translation>Jetzt kaufen für</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#itemUnavailable">
|
<message id="#itemUnavailable">
|
||||||
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="36"/>
|
|
||||||
<source>This item is currently unavailable</source>
|
<source>This item is currently unavailable</source>
|
||||||
<translation>Diese Produkt ist nicht verfügbar</translation>
|
<translation type="vanished">Diese Produkt ist nicht verfügbar</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#thisIsAPremiumFeature">
|
<message id="#thisIsAPremiumFeature">
|
||||||
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="60"/>
|
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="60"/>
|
||||||
|
@ -65,17 +76,18 @@
|
||||||
<translation>Das ist eine premium Funktion.</translation>
|
<translation>Das ist eine premium Funktion.</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#itemIsUnavailable">
|
<message id="#itemIsUnavailable">
|
||||||
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="92"/>
|
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="36"/>
|
||||||
|
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="91"/>
|
||||||
<source>This item is currently unavailable</source>
|
<source>This item is currently unavailable</source>
|
||||||
<translation>Diese Produkt ist nicht verfügbar</translation>
|
<translation>Diese Produkt ist nicht verfügbar</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#restorePurchase">
|
<message id="#restorePurchase">
|
||||||
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="108"/>
|
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="107"/>
|
||||||
<source>Restore purchase</source>
|
<source>Restore purchase</source>
|
||||||
<translation>Kauf wiederherstellen</translation>
|
<translation>Kauf wiederherstellen</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#contact support">
|
<message id="#contact support">
|
||||||
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="119"/>
|
<location filename="../qml/Components/SpeedFlowChartLocker.qml" line="118"/>
|
||||||
<source>contact support</source>
|
<source>contact support</source>
|
||||||
<translation>Support kontaktieren</translation>
|
<translation>Support kontaktieren</translation>
|
||||||
</message>
|
</message>
|
||||||
|
@ -100,17 +112,17 @@
|
||||||
<translation>Über blueROCK</translation>
|
<translation>Über blueROCK</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#ifscDisclaimerTitle">
|
<message id="#ifscDisclaimerTitle">
|
||||||
<location filename="../qml/Pages/StartPage.qml" line="164"/>
|
<location filename="../qml/Pages/StartPage.qml" line="163"/>
|
||||||
<source>Where are the IFSC results?</source>
|
<source>Where are the IFSC results?</source>
|
||||||
<translation>Wo sind die IFSC Ergebnisse?</translation>
|
<translation>Wo sind die IFSC Ergebnisse?</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#ifscDisclaimer">
|
<message id="#ifscDisclaimer">
|
||||||
<location filename="../qml/Pages/StartPage.qml" line="166"/>
|
<location filename="../qml/Pages/StartPage.qml" line="165"/>
|
||||||
<source>Unfortunately, the IFSC has restricted the access to their data and <b>is not willing to share results with blueROCK anymore</b>. Because of this, blueROCK is no longer able to access and display IFSC results.<br><br>You can find current IFSC results <a href="https://ifsc.results.info">over here</a> and on <a href="https://ifsc-climbing.org">their website</a>.</source>
|
<source>Unfortunately, the IFSC has restricted the access to their data and <b>is not willing to share results with blueROCK anymore</b>. Because of this, blueROCK is no longer able to access and display IFSC results.<br><br>You can find current IFSC results <a href="https://ifsc.results.info">over here</a> and on <a href="https://ifsc-climbing.org">their website</a>.</source>
|
||||||
<translation>Leider hat die IFSC den Zugang zu ihren Ergebnissen eingeschränkt<b>und ist nicht mehr bereit, Ergebnisse mit blueROCK zu teilen</b>. Daher ist blueROCK nicht länger in der Lage auf IFSC Ergebnisse zuzugriefen und diese anzuzeigen.<br><br>Aktuelle IFSC Ergebnisse finden sich <a href="https://ifsc.results.info">hier</a> und auf der <a href="https://ifsc-climbing.org">IFSC Webseite</a>.</translation>
|
<translation>Leider hat die IFSC den Zugang zu ihren Ergebnissen eingeschränkt<b>und ist nicht mehr bereit, Ergebnisse mit blueROCK zu teilen</b>. Daher ist blueROCK nicht länger in der Lage auf IFSC Ergebnisse zuzugriefen und diese anzuzeigen.<br><br>Aktuelle IFSC Ergebnisse finden sich <a href="https://ifsc.results.info">hier</a> und auf der <a href="https://ifsc-climbing.org">IFSC Webseite</a>.</translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#aboutBluerockDisclaimer">
|
<message id="#aboutBluerockDisclaimer">
|
||||||
<location filename="../qml/Pages/StartPage.qml" line="174"/>
|
<location filename="../qml/Pages/StartPage.qml" line="173"/>
|
||||||
<source>This app was built using the <a href='https://qt.io'>Qt Framework</a> licensed under the <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html'>GNU lgplV3 license</a>.<br><br>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>.</source>
|
<source>This app was built using the <a href='https://qt.io'>Qt Framework</a> licensed under the <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html'>GNU lgplV3 license</a>.<br><br>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>.</source>
|
||||||
<translation type="unfinished">Diese App wurde unter Verwendung des <a href='https://qt.io'>Qt Frameworks</a> unter der <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html'>GNU lgplV3 Lizenz</a> erstellt.<br><br>Diese App ist Open-source und lizensiert unter der <a href='https://www.gnu.org/licenses/agpl-3.0.en.html'>GNU agplV3 Lizenz</a>. Der Sourcecode findet sich <a href='https://itsblue.dev/dorian/blueROCK/'>hier</a>.Die Ergebnisse und Ranglisten werden von <a href='http://www.digitalROCK.de'>digital ROCK</a> zur Verfügung gestellt.</translation>
|
<translation type="unfinished">Diese App wurde unter Verwendung des <a href='https://qt.io'>Qt Frameworks</a> unter der <a href='https://www.gnu.org/licenses/lgpl-3.0.en.html'>GNU lgplV3 Lizenz</a> erstellt.<br><br>Diese App ist Open-source und lizensiert unter der <a href='https://www.gnu.org/licenses/agpl-3.0.en.html'>GNU agplV3 Lizenz</a>. Der Sourcecode findet sich <a href='https://itsblue.dev/dorian/blueROCK/'>hier</a>.Die Ergebnisse und Ranglisten werden von <a href='http://www.digitalROCK.de'>digital ROCK</a> zur Verfügung gestellt.</translation>
|
||||||
</message>
|
</message>
|
||||||
|
|
Binary file not shown.
|
@ -10,17 +10,28 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#placeQrCodeInCenter">
|
<message id="#placeQrCodeInCenter">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="41"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="44"/>
|
||||||
<source>Place the Code in the center</source>
|
<source>Place the Code in the center</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
<message id="#cameraPermissionDenied">
|
||||||
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="208"/>
|
||||||
|
<source>Camera permission denied!</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message id="#cameraPermissionDeniedDetails">
|
||||||
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="228"/>
|
||||||
|
<source>This app requires access to your camera in order to scan QR-Codes. It will never record or store any photos or videos.</source>
|
||||||
|
<oldsource>This app requires access to your camera in order to scan QR-Codes. It will never record store any photos or videos.</oldsource>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
<message id="#pleaseWait">
|
<message id="#pleaseWait">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="183"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="245"/>
|
||||||
<source>Plase wait</source>
|
<source>Plase wait</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message id="#invalidQrCode">
|
<message id="#invalidQrCode">
|
||||||
<location filename="../qml/Components/QrCodeScanPopup.qml" line="189"/>
|
<location filename="../qml/Components/QrCodeScanPopup.qml" line="251"/>
|
||||||
<source>Invalid QR-Code</source>
|
<source>Invalid QR-Code</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
|
|
|
@ -130,8 +130,10 @@ void BlueRockBackend::shareResultsAsPoster(QString url, QString compName) {
|
||||||
|
|
||||||
QPixmap barcode;
|
QPixmap barcode;
|
||||||
int size = writer.width() * 0.5;
|
int size = writer.width() * 0.5;
|
||||||
|
|
||||||
QZXingEncoderConfig encoderConfig(QZXing::EncoderFormat_QR_CODE, QSize(size, size), QZXing::EncodeErrorCorrectionLevel_M, false, false);
|
QZXingEncoderConfig encoderConfig(QZXing::EncoderFormat_QR_CODE, QSize(size, size), QZXing::EncodeErrorCorrectionLevel_M, false, false);
|
||||||
barcode.convertFromImage(QZXing::encodeData(url, encoderConfig));
|
barcode.convertFromImage(QZXing::encodeData(url, encoderConfig));
|
||||||
|
|
||||||
painter.drawPixmap((writer.width() - size) / 2, size * 0.5, size, size, barcode);
|
painter.drawPixmap((writer.width() - size) / 2, size * 0.5, size, size, barcode);
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
|
@ -140,6 +142,73 @@ void BlueRockBackend::shareResultsAsPoster(QString url, QString compName) {
|
||||||
this->_shareUtils->sendFile(path, compName, "application/pdf", 1);
|
this->_shareUtils->sendFile(path, compName, "application/pdf", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BlueRockBackend::isCameraPermissionGranted() {
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
QtAndroid::PermissionResult cameraAccess = QtAndroid::checkPermission("android.permission.CAMERA");
|
||||||
|
return cameraAccess == QtAndroid::PermissionResult::Granted;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BlueRockBackend::requestCameraPermission() {
|
||||||
|
if(this->isCameraPermissionGranted())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
// try to get permission
|
||||||
|
QtAndroid::PermissionResultMap resultMap = QtAndroid::requestPermissionsSync({"android.permission.CAMERA"});
|
||||||
|
bool resultBool = true;
|
||||||
|
for(QtAndroid::PermissionResult result : resultMap) {
|
||||||
|
if(result != QtAndroid::PermissionResult::Granted) {
|
||||||
|
resultBool = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(resultBool) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getting permission the traditional way failed -> open the settings app
|
||||||
|
QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;");
|
||||||
|
if (activity.isValid())
|
||||||
|
{
|
||||||
|
// get the package name
|
||||||
|
QAndroidJniObject context = QtAndroid::androidContext();
|
||||||
|
QAndroidJniObject applicationPackageName = context.callObjectMethod<jstring>("getPackageName");
|
||||||
|
|
||||||
|
QAndroidJniObject param = QAndroidJniObject::fromString("package:" + applicationPackageName.toString());
|
||||||
|
|
||||||
|
// Equivalent to Jave code: 'Uri uri = Uri::parse("...");'
|
||||||
|
QAndroidJniObject uri = QAndroidJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", param.object<jstring>());
|
||||||
|
if (!uri.isValid()) {
|
||||||
|
qWarning("ERROR: Unable to create Uri object");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QAndroidJniObject packageName = QAndroidJniObject::fromString("android.settings.APPLICATION_DETAILS_SETTINGS");
|
||||||
|
|
||||||
|
QAndroidJniObject intent("android/content/Intent","(Ljava/lang/String;)V", packageName.object<jstring>());
|
||||||
|
if (!intent.isValid()) {
|
||||||
|
qWarning("ERROR: Unable to create Intent object");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
intent.callObjectMethod("addCategory", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString("android.intent.category.DEFAULT").object<jstring>());
|
||||||
|
intent.callObjectMethod("setData", "(Landroid/net/Uri;)Landroid/content/Intent;", uri.object<jobject>());
|
||||||
|
|
||||||
|
activity.callMethod<void>("startActivity","(Landroid/content/Intent;)V",intent.object<jobject>());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "ERROR: Activity not valid!";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
// --- Helper functions ---
|
// --- Helper functions ---
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
|
Loading…
Reference in a new issue