diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7aa25bb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "qzxing"] + path = qzxing + url = https://github.com/ftylitak/qzxing.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d2788..5712cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +# [0.6.1] - 2021-10-20 +### Added +- Privacy policy link to comply with google play policies + +# [0.6.0] - 2021-08-07 +### Changed +- The subtitle in results and startlists is now the route name instead of the category name + +### Added +- Dark mode +- QR-Code scanning +- Sharing of every view using either link, QR-Code or a poster +- Text which is too large too fit is scrollable now in most places +- German translations +- URL handler for https://l.bluerock.dev and https://app.bluerock.dev + +### Fixed +- Rare issue with missing background in boulder result rect + # [0.5.1] - 2021-07-06 ### Fixed - In-app purchase @@ -20,7 +39,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - the calendar now scrolls less far down - improoved layout in landscape mode - some design changes in profile page and speed flowchart -- added second link for "further infos" in calendar + +### Added +- Second link for "further infos" in calendar # [0.03.0] - 2019-07-11 ### Added diff --git a/README.md b/README.md index 4aa83b8..932984f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,20 @@ # Digital Rock ranking -App to view ranking and calendar data from https://www.digitalrock.de/ \ No newline at end of file +App to view ranking and calendar data from https://www.digitalrock.de/ + +# Init +´´´ +git submodule init +git submodule update +´´´ + +# Poster +The poster offsets are (always top left of the element): +- Width: 1654 +- Height: 2339 +### QR-Code +- Cooridnates: 414, 414 +- Size: 1650x1650 +### Comp name +- Cooridnates: x: 324, y: 2500 +- Size: 64 per line; 1835 width; 150 max height diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index ec293df..9f1c607 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -10,7 +10,7 @@ - + @@ -68,9 +68,23 @@ --> + + + + + + + + + + + + + + diff --git a/android/build.gradle b/android/build.gradle index 0051ff0..7ef94c3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -18,6 +18,7 @@ apply plugin: 'com.android.application' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + compile 'com.android.support:support-v4:25.3.1' } android { @@ -72,10 +73,11 @@ android { defaultConfig { resConfig "en" minSdkVersion = qtMinSdkVersion - targetSdkVersion = 29 + targetSdkVersion = qtTargetSdkVersion } lintOptions { checkReleaseBuilds false + abortOnError false } } diff --git a/android/res/xml/filepaths.xml b/android/res/xml/filepaths.xml new file mode 100644 index 0000000..6a45457 --- /dev/null +++ b/android/res/xml/filepaths.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/src/de/itsblue/blueROCK/MainActivity.java b/android/src/de/itsblue/blueROCK/MainActivity.java new file mode 100755 index 0000000..c070a26 --- /dev/null +++ b/android/src/de/itsblue/blueROCK/MainActivity.java @@ -0,0 +1,232 @@ +// (c) 2017 Ekkehard Gentz (ekke) +// this project is based on ideas from +// http://blog.lasconic.com/share-on-ios-and-android-using-qml/ +// see github project https://github.com/lasconic/ShareUtils-QML +// also inspired by: +// https://www.androidcode.ninja/android-share-intent-example/ +// https://www.calligra.org/blogs/sharing-with-qt-on-android/ +// https://stackoverflow.com/questions/7156932/open-file-in-another-app +// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData +// OpenURL in At Android: got ideas from: +// https://github.com/BernhardWenzel/open-url-in-qt-android +// https://github.com/tobiatesan/android_intents_qt +// +// see also /COPYRIGHT and /LICENSE + +package de.itsblue.blueROCK; + +import org.qtproject.qt5.android.QtNative; + +import org.qtproject.qt5.android.bindings.QtActivity; +import android.os.*; +import android.content.*; +import android.app.*; + +import java.lang.String; +import android.content.Intent; +import java.io.File; +import android.net.Uri; +import android.util.Log; +import android.content.ContentResolver; +import android.webkit.MimeTypeMap; + +import org.ekkescorner.utils.*; + + + +public class MainActivity extends QtActivity +{ + // native - must be implemented in Cpp via JNI + // 'file' scheme or resolved from 'content' scheme: + public static native void setFileUrlReceived(String url); + // + public static native void setOtherUrlReceived(String url, String scheme); + // InputStream from 'content' scheme: + public static native void setFileReceivedAndSaved(String url); + // + public static native void fireActivityResult(int requestCode, int resultCode); + // + public static native boolean checkFileExits(String url); + + public static boolean isIntentPending; + public static boolean isInitialized; + public static String workingDirPath; + + // Use a custom Chooser without providing own App as share target ! + // see QShareUtils.java createCustomChooserAndStartActivity() + // Selecting your own App as target could cause AndroidOS to call + // onCreate() instead of onNewIntent() + // and then you are in trouble because we're using 'singleInstance' as LaunchMode + // more details: my blog at Qt + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Log.d("ekkescorner", "onCreate QShareActivity"); + // now we're checking if the App was started from another Android App via Intent + Intent theIntent = getIntent(); + if (theIntent != null){ + String theAction = theIntent.getAction(); + if (theAction != null){ + Log.d("ekkescorner onCreate ", theAction); + // QML UI not ready yet + // delay processIntent(); + isIntentPending = true; + } + } + } // onCreate + + // WIP - trying to find a solution to survive a 2nd onCreate + // ongoing discussion in QtMob (Slack) + // from other Apps not respecting that you only have a singleInstance + // there are problems per ex. sharing a file from Google Files App, + // but working well using Xiaomi FileManager App + @Override + public void onDestroy() { + Log.d("ekkescorner", "onDestroy QShareActivity"); + // super.onDestroy(); + // System.exit() closes the App before doing onCreate() again + // then the App was restarted, but looses context + // This works for Samsung My Files + // but Google Files doesn't call onDestroy() + System.exit(0); + } + + // we start Activity with result code + // to test JNI with QAndroidActivityResultReceiver you must comment or rename + // this method here - otherwise you'll get wrong request or result codes + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + // Check which request we're responding to + Log.d("ekkescorner onActivityResult", "requestCode: "+requestCode); + if (resultCode == RESULT_OK) { + Log.d("ekkescorner onActivityResult - resultCode: ", "SUCCESS"); + } else { + Log.d("ekkescorner onActivityResult - resultCode: ", "CANCEL"); + } + // hint: result comes back too fast for Action SEND + // if you want to delete/move the File add a Timer w 500ms delay + // see Example App main.qml - delayDeleteTimer + // if you want to revoke permissions for older OS + // it makes sense also do this after the delay + fireActivityResult(requestCode, resultCode); + } + + // if we are opened from other apps: + @Override + public void onNewIntent(Intent intent) { + Log.d("ekkescorner", "onNewIntent"); + super.onNewIntent(intent); + setIntent(intent); + // Intent will be processed, if all is initialized and Qt / QML can handle the event + if(isInitialized) { + processIntent(); + } else { + isIntentPending = true; + } + } // onNewIntent + + public void checkPendingIntents(String workingDir) { + isInitialized = true; + workingDirPath = workingDir; + Log.d("ekkescorner", workingDirPath); + if(isIntentPending) { + isIntentPending = false; + Log.d("ekkescorner", "checkPendingIntents: true"); + processIntent(); + } else { + Log.d("ekkescorner", "nothingPending"); + } + } // checkPendingIntents + + // process the Intent if Action is SEND or VIEW + private void processIntent(){ + Intent intent = getIntent(); + + Uri intentUri; + String intentScheme; + String intentAction; + // we are listening to android.intent.action.SEND or VIEW (see Manifest) + if (intent.getAction().equals("android.intent.action.VIEW")){ + intentAction = "VIEW"; + intentUri = intent.getData(); + } else if (intent.getAction().equals("android.intent.action.SEND")){ + intentAction = "SEND"; + Bundle bundle = intent.getExtras(); + intentUri = (Uri)bundle.get(Intent.EXTRA_STREAM); + } else { + Log.d("ekkescorner Intent unknown action:", intent.getAction()); + return; + } + Log.d("ekkescorner action:", intentAction); + if (intentUri == null){ + Log.d("ekkescorner Intent URI:", "is null"); + return; + } + + Log.d("ekkescorner Intent URI:", intentUri.toString()); + + // content or file + intentScheme = intentUri.getScheme(); + if (intentScheme == null){ + Log.d("ekkescorner Intent URI Scheme:", "is null"); + return; + } + if(intentScheme.equals("file")){ + // URI as encoded string + Log.d("ekkescorner Intent File URI: ", intentUri.toString()); + setFileUrlReceived(intentUri.toString()); + // we are done Qt can deal with file scheme + return; + } + if(!intentScheme.equals("content")){ + Log.d("ekkescorner Intent URI unknown scheme: ", intentScheme); + setOtherUrlReceived(intentUri.toString(), intentScheme); + return; + } + // ok - it's a content scheme URI + // we will try to resolve the Path to a File URI + // if this won't work or if the File cannot be opened, + // we'll try to copy the file into our App working dir via InputStream + // hopefully in most cases PathResolver will give a path + + // you need the file extension, MimeType or Name from ContentResolver ? + // here's HowTo get it: + Log.d("ekkescorner Intent Content URI: ", intentUri.toString()); + ContentResolver cR = this.getContentResolver(); + MimeTypeMap mime = MimeTypeMap.getSingleton(); + String fileExtension = mime.getExtensionFromMimeType(cR.getType(intentUri)); + Log.d("ekkescorner","Intent extension: "+fileExtension); + String mimeType = cR.getType(intentUri); + Log.d("ekkescorner"," Intent MimeType: "+mimeType); + String name = QShareUtils.getContentName(cR, intentUri); + if(name != null) { + Log.d("ekkescorner Intent Name:", name); + } else { + Log.d("ekkescorner Intent Name:", "is NULL"); + } + String filePath; + filePath = QSharePathResolver.getRealPathFromURI(this, intentUri); + if(filePath == null) { + Log.d("ekkescorner QSharePathResolver:", "filePath is NULL"); + } else { + Log.d("ekkescorner QSharePathResolver:", filePath); + // to be safe check if this File Url really can be opened by Qt + // there were problems with MS office apps on Android 7 + if (checkFileExits(filePath)) { + setFileUrlReceived(filePath); + // we are done Qt can deal with file scheme + return; + } + } + + // trying the InputStream way: + filePath = QShareUtils.createFile(cR, intentUri, workingDirPath); + if(filePath == null) { + Log.d("ekkescorner Intent FilePath:", "is NULL"); + return; + } + setFileReceivedAndSaved(filePath); + } // processIntent + +} // class QShareActivity diff --git a/android/src/org/ekkescorner/utils/QSharePathResolver.java b/android/src/org/ekkescorner/utils/QSharePathResolver.java new file mode 100644 index 0000000..f93d1a8 --- /dev/null +++ b/android/src/org/ekkescorner/utils/QSharePathResolver.java @@ -0,0 +1,223 @@ +// from: https://github.com/wkh237/react-native-fetch-blob/blob/master/android/src/main/java/com/RNFetchBlob/Utils/PathResolver.java +// MIT License, see: https://github.com/wkh237/react-native-fetch-blob/blob/master/LICENSE +// original copyright: Copyright (c) 2017 xeiyan@gmail.com +// src slightly modified to be used into Qt Projects: (c) 2017 ekke@ekkes-corner.org + +package org.ekkescorner.utils; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.content.ContentUris; +import android.os.Environment; +import android.content.ContentResolver; +import java.io.File; +import java.io.InputStream; +import java.io.FileOutputStream; +import android.util.Log; +import java.lang.NumberFormatException; + +public class QSharePathResolver { + public static String getRealPathFromURI(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + Log.d("ekkescorner"," isExternalStorageDocument"); + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + Log.d("ekkescorner"," isDownloadsDocument"); + final String id = DocumentsContract.getDocumentId(uri); + Log.d("ekkescorner"," getDocumentId "+id); + long longId = 0; + try + { + longId = Long.valueOf(id); + } + catch(NumberFormatException nfe) + { + return getDataColumn(context, uri, null, null); + } + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), longId); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + Log.d("ekkescorner"," isMediaDocument"); + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + else if ("content".equalsIgnoreCase(uri.getScheme())) { + Log.d("ekkescorner"," is uri.getScheme()"); + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // Other Providers + else{ + Log.d("ekkescorner ","is Other Provider"); + try { + InputStream attachment = context.getContentResolver().openInputStream(uri); + if (attachment != null) { + String filename = getContentName(context.getContentResolver(), uri); + if (filename != null) { + File file = new File(context.getCacheDir(), filename); + FileOutputStream tmp = new FileOutputStream(file); + byte[] buffer = new byte[1024]; + while (attachment.read(buffer) > 0) { + tmp.write(buffer); + } + tmp.close(); + attachment.close(); + return file.getAbsolutePath(); + } + } + } catch (Exception e) { + // TODO SIGNAL shareError() + return null; + } + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri"); + Log.d("ekkescorner"," is uri.getScheme()"); + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + Log.d("ekkescorner"," return: getDataColumn "); + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + Log.d("ekkescorner ","NOT DocumentsContract.isDocumentUri"); + Log.d("ekkescorner"," is file scheme"); + return uri.getPath(); + } + + return null; + } + + private static String getContentName(ContentResolver resolver, Uri uri) { + Cursor cursor = resolver.query(uri, null, null, null, null); + cursor.moveToFirst(); + int nameIndex = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + String name = cursor.getString(nameIndex); + cursor.close(); + return name; + } + cursor.close(); + return null; + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + String result = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int index = cursor.getColumnIndexOrThrow(column); + result = cursor.getString(index); + } + } + catch (Exception ex) { + ex.printStackTrace(); + return null; + } + finally { + if (cursor != null) + cursor.close(); + } + return result; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + +} diff --git a/android/src/org/ekkescorner/utils/QShareUtils.java b/android/src/org/ekkescorner/utils/QShareUtils.java new file mode 100755 index 0000000..38a07a7 --- /dev/null +++ b/android/src/org/ekkescorner/utils/QShareUtils.java @@ -0,0 +1,396 @@ +// (c) 2017 Ekkehard Gentz (ekke) +// this project is based on ideas from +// http://blog.lasconic.com/share-on-ios-and-android-using-qml/ +// see github project https://github.com/lasconic/ShareUtils-QML +// also inspired by: +// https://www.androidcode.ninja/android-share-intent-example/ +// https://www.calligra.org/blogs/sharing-with-qt-on-android/ +// https://stackoverflow.com/questions/7156932/open-file-in-another-app +// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData +// https://stackoverflow.com/questions/5734678/custom-filtering-of-intent-chooser-based-on-installed-android-package-name +// see also /COPYRIGHT and /LICENSE + +package org.ekkescorner.utils; + +import org.qtproject.qt5.android.QtNative; + +import java.lang.String; +import android.content.Intent; +import java.io.File; +import android.net.Uri; +import android.util.Log; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.provider.MediaStore; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.FileOutputStream; + +import java.util.List; +import android.content.pm.ResolveInfo; +import java.util.ArrayList; +import android.content.pm.PackageManager; +import java.util.Comparator; +import java.util.Collections; +import android.content.Context; +import android.os.Parcelable; + +import android.os.Build; + +import android.support.v4.content.FileProvider; +import android.support.v4.app.ShareCompat; + +public class QShareUtils +{ + // reference Authority as defined in AndroidManifest.xml + private static String AUTHORITY="de.itsblue.blueROCK.fileprovider"; + + protected QShareUtils() + { + Log.d("ekkescorner", "QShareUtils()"); + } + + public static boolean checkMimeTypeView(String mimeType) { + if (QtNative.activity() == null) + return false; + Intent myIntent = new Intent(); + myIntent.setAction(Intent.ACTION_VIEW); + // without an URI resolve always fails + // an empty URI allows to resolve the Activity + File fileToShare = new File(""); + Uri uri = Uri.fromFile(fileToShare); + myIntent.setDataAndType(uri, mimeType); + + // Verify that the intent will resolve to an activity + if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { + Log.d("ekkescorner checkMime ", "YEP - we can go on and View"); + return true; + } else { + Log.d("ekkescorner checkMime", "sorry - no App available to View"); + } + return false; + } + + public static boolean checkMimeTypeEdit(String mimeType) { + if (QtNative.activity() == null) + return false; + Intent myIntent = new Intent(); + myIntent.setAction(Intent.ACTION_EDIT); + // without an URI resolve always fails + // an empty URI allows to resolve the Activity + File fileToShare = new File(""); + Uri uri = Uri.fromFile(fileToShare); + myIntent.setDataAndType(uri, mimeType); + + // Verify that the intent will resolve to an activity + if (myIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { + Log.d("ekkescorner checkMime ", "YEP - we can go on and Edit"); + return true; + } else { + Log.d("ekkescorner checkMime", "sorry - no App available to Edit"); + } + return false; + } + + public static boolean shareText(String text) { + if (QtNative.activity() == null) + return false; + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, text); + sendIntent.setType("text/plain"); + + // Verify that the intent will resolve to an activity + if (sendIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { + QtNative.activity().startActivity(sendIntent); + return true; + } else { + Log.d("ekkescorner share", "Intent not resolved"); + } + return false; + } + + // thx @oxied and @pooks for the idea: https://stackoverflow.com/a/18835895/135559 + // theIntent is already configured with all needed properties and flags + // so we only have to add the packageName of targeted app + public static boolean createCustomChooserAndStartActivity(Intent theIntent, String title, int requestId, Uri uri) { + final Context context = QtNative.activity(); + final PackageManager packageManager = context.getPackageManager(); + final boolean isLowerOrEqualsKitKat = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT; + + // MATCH_DEFAULT_ONLY: Resolution and querying flag. if set, only filters that support the CATEGORY_DEFAULT will be considered for matching. + // Check if there is a default app for this type of content. + ResolveInfo defaultAppInfo = packageManager.resolveActivity(theIntent, PackageManager.MATCH_DEFAULT_ONLY); + if(defaultAppInfo == null) { + Log.d("ekkescorner", title+" PackageManager cannot resolve Activity"); + return false; + } + + // had to remove this check - there can be more Activity names, per ex + // com.google.android.apps.docs.editors.kix.quickword.QuickWordDocumentOpenerActivityAlias + // if (!defaultAppInfo.activityInfo.name.endsWith("ResolverActivity") && !defaultAppInfo.activityInfo.name.endsWith("EditActivity")) { + // Log.d("ekkescorner", title+" defaultAppInfo not Resolver or EditActivity: "+defaultAppInfo.activityInfo.name); + // return false; + //} + + // Retrieve all apps for our intent. Check if there are any apps returned + List appInfoList = packageManager.queryIntentActivities(theIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (appInfoList.isEmpty()) { + Log.d("ekkescorner", title+" appInfoList.isEmpty"); + return false; + } + Log.d("ekkescorner", title+" appInfoList: "+appInfoList.size()); + + // Sort in alphabetical order + Collections.sort(appInfoList, new Comparator() { + @Override + public int compare(ResolveInfo first, ResolveInfo second) { + String firstName = first.loadLabel(packageManager).toString(); + String secondName = second.loadLabel(packageManager).toString(); + return firstName.compareToIgnoreCase(secondName); + } + }); + + List targetedIntents = new ArrayList(); + // Filter itself and create intent with the rest of the apps. + for (ResolveInfo appInfo : appInfoList) { + // get the target PackageName + String targetPackageName = appInfo.activityInfo.packageName; + // we don't want to share with our own app + // in fact sharing with own app with resultCode will crash because doesn't work well with launch mode 'singleInstance' + if (targetPackageName.equals(context.getPackageName())) { + continue; + } + // if you have a blacklist of apps please exclude them here + + // we create the targeted Intent based on our already configured Intent + Intent targetedIntent = new Intent(theIntent); + // now add the target packageName so this Intent will only find the one specific App + targetedIntent.setPackage(targetPackageName); + // collect all these targetedIntents + targetedIntents.add(targetedIntent); + + // legacy support and Workaround for Android bug + // grantUriPermission needed for KITKAT or older + // see https://code.google.com/p/android/issues/detail?id=76683 + // also: https://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps + if(isLowerOrEqualsKitKat) { + Log.d("ekkescorner", "legacy support grantUriPermission"); + context.grantUriPermission(targetPackageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + // attention: you must revoke the permission later, so this only makes sense with getting back a result to know that Intent was done + // I always move or delete the file, so I don't revoke permission + } + } + + // check if there are apps found for our Intent to avoid that there was only our own removed app before + if (targetedIntents.isEmpty()) { + Log.d("ekkescorner", title+" targetedIntents.isEmpty"); + return false; + } + + // now we can create our Intent with custom Chooser + // we need all collected targetedIntents as EXTRA_INITIAL_INTENTS + // we're using the last targetedIntent as initializing Intent, because + // chooser adds its initializing intent to the end of EXTRA_INITIAL_INTENTS :) + Intent chooserIntent = Intent.createChooser(targetedIntents.remove(targetedIntents.size() - 1), title); + if (targetedIntents.isEmpty()) { + Log.d("ekkescorner", title+" only one Intent left for Chooser"); + } else { + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toArray(new Parcelable[] {})); + } + // Verify that the intent will resolve to an activity + if (chooserIntent.resolveActivity(QtNative.activity().getPackageManager()) != null) { + if(requestId > 0) { + QtNative.activity().startActivityForResult(chooserIntent, requestId); + } else { + QtNative.activity().startActivity(chooserIntent); + } + return true; + } + Log.d("ekkescorner", title+" Chooser Intent not resolved. Should never happen"); + return false; + } + + // I am deleting the files from shared folder when Activity was done or canceled + // so probably I don't have to revike FilePermissions for older OS + // if you don't delete or move the file: here's what you must done to revoke the access + public static void revokeFilePermissions(String filePath) { + final Context context = QtNative.activity(); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + File file = new File(filePath); + Uri uri = FileProvider.getUriForFile(context, AUTHORITY, file); + context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } + } + + public static boolean sendFile(String filePath, String title, String mimeType, int requestId) { + if (QtNative.activity() == null) + return false; + + // using v4 support library create the Intent from ShareCompat + // Intent sendIntent = new Intent(); + Intent sendIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); + sendIntent.setAction(Intent.ACTION_SEND); + + File imageFileToShare = new File(filePath); + + // Using FileProvider you must get the URI from FileProvider using your AUTHORITY + // Uri uri = Uri.fromFile(imageFileToShare); + Uri uri; + try { + uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); + } catch (IllegalArgumentException e) { + Log.d("ekkescorner sendFile - cannot be shared: ", filePath); + return false; + } + + Log.d("ekkescorner sendFile", uri.toString()); + sendIntent.putExtra(Intent.EXTRA_STREAM, uri); + + if(mimeType == null || mimeType.isEmpty()) { + // fallback if mimeType not set + mimeType = QtNative.activity().getContentResolver().getType(uri); + Log.d("ekkescorner sendFile guessed mimeType:", mimeType); + } else { + Log.d("ekkescorner sendFile w mimeType:", mimeType); + } + + sendIntent.setType(mimeType); + + sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + sendIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + return createCustomChooserAndStartActivity(sendIntent, title, requestId, uri); + } + + public static boolean viewFile(String filePath, String title, String mimeType, int requestId) { + if (QtNative.activity() == null) + return false; + + // using v4 support library create the Intent from ShareCompat + // Intent viewIntent = new Intent(); + Intent viewIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); + viewIntent.setAction(Intent.ACTION_VIEW); + + File imageFileToShare = new File(filePath); + + // Using FileProvider you must get the URI from FileProvider using your AUTHORITY + // Uri uri = Uri.fromFile(imageFileToShare); + Uri uri; + try { + uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); + } catch (IllegalArgumentException e) { + Log.d("ekkescorner viewFile - cannot be shared: ", filePath); + return false; + } + // now we got a content URI per ex + // content://org.ekkescorner.examples.sharex.fileprovider/my_shared_files/qt-logo.png + // from a fileUrl: + // /data/user/0/org.ekkescorner.examples.sharex/files/share_example_x_files/qt-logo.png + Log.d("ekkescorner viewFile from file path: ", filePath); + Log.d("ekkescorner viewFile to content URI: ", uri.toString()); + + if(mimeType == null || mimeType.isEmpty()) { + // fallback if mimeType not set + mimeType = QtNative.activity().getContentResolver().getType(uri); + Log.d("ekkescorner viewFile guessed mimeType:", mimeType); + } else { + Log.d("ekkescorner viewFile w mimeType:", mimeType); + } + + viewIntent.setDataAndType(uri, mimeType); + + viewIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + viewIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + return createCustomChooserAndStartActivity(viewIntent, title, requestId, uri); + } + + public static boolean editFile(String filePath, String title, String mimeType, int requestId) { + if (QtNative.activity() == null) + return false; + + // using v4 support library create the Intent from ShareCompat + // Intent editIntent = new Intent(); + Intent editIntent = ShareCompat.IntentBuilder.from(QtNative.activity()).getIntent(); + editIntent.setAction(Intent.ACTION_EDIT); + + File imageFileToShare = new File(filePath); + + // Using FileProvider you must get the URI from FileProvider using your AUTHORITY + // Uri uri = Uri.fromFile(imageFileToShare); + Uri uri; + try { + uri = FileProvider.getUriForFile(QtNative.activity(), AUTHORITY, imageFileToShare); + } catch (IllegalArgumentException e) { + Log.d("ekkescorner editFile - cannot be shared: ", filePath); + return false; + } + Log.d("ekkescorner editFile", uri.toString()); + + if(mimeType == null || mimeType.isEmpty()) { + // fallback if mimeType not set + mimeType = QtNative.activity().getContentResolver().getType(uri); + Log.d("ekkescorner editFile guessed mimeType:", mimeType); + } else { + Log.d("ekkescorner editFile w mimeType:", mimeType); + } + + editIntent.setDataAndType(uri, mimeType); + + editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + + return createCustomChooserAndStartActivity(editIntent, title, requestId, uri); + } + + public static String getContentName(ContentResolver cR, Uri uri) { + Cursor cursor = cR.query(uri, null, null, null, null); + cursor.moveToFirst(); + int nameIndex = cursor + .getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME); + if (nameIndex >= 0) { + return cursor.getString(nameIndex); + } else { + return null; + } + } + + public static String createFile(ContentResolver cR, Uri uri, String fileLocation) { + String filePath = null; + try { + InputStream iStream = cR.openInputStream(uri); + if (iStream != null) { + String name = getContentName(cR, uri); + if (name != null) { + filePath = fileLocation + "/" + name; + Log.d("ekkescorner - create File", filePath); + File f = new File(filePath); + FileOutputStream tmp = new FileOutputStream(f); + Log.d("ekkescorner - create File", "new FileOutputStream"); + + byte[] buffer = new byte[1024]; + while (iStream.read(buffer) > 0) { + tmp.write(buffer); + } + tmp.close(); + iStream.close(); + return filePath; + } // name + } // iStream + } catch (FileNotFoundException e) { + e.printStackTrace(); + return filePath; + } catch (IOException e) { + e.printStackTrace(); + return filePath; + } catch (Exception e) { + e.printStackTrace(); + return filePath; + } + return filePath; + } + +} diff --git a/blueROCK.pro b/blueROCK.pro index e4ff89f..a6949d2 100644 --- a/blueROCK.pro +++ b/blueROCK.pro @@ -1,7 +1,7 @@ -QT += quick qml quickcontrols2 purchasing widgets +QT += quick qml quickcontrols2 purchasing CONFIG += c++11 -VERSION = 0.5.1 +VERSION = 0.6.1 TARGET = blueROCK # The following define makes your compiler emit warnings if you use @@ -15,16 +15,30 @@ DEFINES += QT_DEPRECATED_WARNINGS # You can also select to disable deprecated APIs only up to a certain version of Qt. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 +INCLUDEPATH += $$PWD/headers + +# Add version to define +DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\" + SOURCES += \ + sources/shareUtils/platformshareutils.cpp \ sources/appsettings.cpp \ - sources/main.cpp \ - sources/serverconn.cpp + sources/bluerockbackend.cpp \ + sources/shareUtils/shareutils.cpp \ + sources/main.cpp + +HEADERS += \ + headers/appsettings.h \ + headers/bluerockbackend.h \ + headers/shareUtils/shareutils.h \ + headers/shareUtils/platformshareutils.h RESOURCES += resources/qml/qml.qrc \ resources/shared/shared.qrc \ - #resources/shared/icons/bluerock/index.theme \ - #$$files(resources/shared/icons/*.png, true) + resources/translations/translations.qrc +TRANSLATIONS += resources/translations/en.ts \ + resources/translations/de.ts # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH = @@ -37,27 +51,29 @@ qnx: target.path = /tmp/$${TARGET}/bin else: unix:!android: target.path = /opt/$${TARGET}/bin !isEmpty(target.path): INSTALLS += target -# Add version to define -DEFINES += APP_VERSION=\"\\\"$${VERSION}\\\"\" - -HEADERS += \ - headers/appsettings.h \ - headers/serverconn.h - DISTFILES += \ CHANGELOG.md \ - android/AndroidManifest.xml \ - android/build.gradle \ - android/gradle.properties \ - android/gradle/wrapper/gradle-wrapper.jar \ - android/gradle/wrapper/gradle-wrapper.properties \ - android/gradlew \ - android/gradlew.bat \ - android/res/values/libs.xml + README.md android { QT += androidextras + SOURCES += sources/shareUtils/androidshareutils.cpp + HEADERS += headers/shareUtils/androidshareutils.h + + OTHER_FILES += android/src/org/ekkescorner/utils/QShareUtils.java \ + android/src/org/ekkescorner/utils/QSharePathResolver.java \ + android/src/de/itsblue/blueROCK/MainActivity.java \ + android/AndroidManifest.xml \ + android/build.gradle \ + android/gradle.properties \ + android/gradle/wrapper/gradle-wrapper.jar \ + android/gradle/wrapper/gradle-wrapper.properties \ + android/gradlew \ + android/gradlew.bat \ + android/res/values/libs.xml \ + android/res/xml/filepaths.xml + defineReplace(droidVersionCode) { segments = $$split(1, ".") for (segment, segments): vCode = "$$first(vCode)$$format_number($$segment, width=3 zeropad)" @@ -73,17 +89,63 @@ android { ANDROID_VERSION_NAME = $$VERSION ANDROID_VERSION_CODE = $$droidVersionCode($$ANDROID_VERSION_NAME) - message(Android version code: $$ANDROID_VERSION_CODE) + ANDROID_TARGET_SDK_VERSION = 29 include(/home/dorian/Android/Sdk/android_openssl/openssl.pri) ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android } ios { + OBJECTIVE_SOURCES += sources/shareUtils/ios/iosshareutils.mm \ + sources/iospermissionutils.mm \ + sources/shareUtils/ios/docviewcontroller.mm + + HEADERS += headers/shareUtils/ios/iosshareutils.h \ + headers/iospermissionutils.h \ + headers/shareUtils/ios/docviewcontroller.h + + OTHER_FILES += ios/Info.plist \ + ios/blueROCK.entitlements + + QMAKE_INFO_PLIST = ios/Info.plist + + + #QMAKE_IOS_DEPLOYMENT_TARGET = 12.0 + + #disable_warning.name = GCC_WARN_64_TO_32_BIT_CONVERSION + #disable_warning.value = NO + #QMAKE_MAC_XCODE_SETTINGS += disable_warning + + # see https://bugreports.qt.io/browse/QTCREATORBUG-16968 + # ios_signature.pri not part of project repo because of private signature details + # contains: + # QMAKE_XCODE_CODE_SIGN_IDENTITY = "iPhone Developer" + # MY_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM + # MY_DEVELOPMENT_TEAM.value = your team Id from Apple Developer Account + # QMAKE_MAC_XCODE_SETTINGS += MY_DEVELOPMENT_TEAM + + #include(ios_signature.pri) + QMAKE_ASSET_CATALOGS += resources/shared/Assets.xcassets + + MY_ENTITLEMENTS.name = CODE_SIGN_ENTITLEMENTS + MY_ENTITLEMENTS.value = $$PWD/ios/blueROCK.entitlements + QMAKE_MAC_XCODE_SETTINGS += MY_ENTITLEMENTS + + MY_BUNDLE_ID.name = PRODUCT_BUNDLE_IDENTIFIER + MY_BUNDLE_ID.value = de.itsblue.bluerock + QMAKE_MAC_XCODE_SETTINGS += MY_BUNDLE_ID xcode_product_bundle_identifier_setting.value = "de.itsblue.bluerock" + PRODUCT_IDENTIFIER = 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/appsettings.h b/headers/appsettings.h index 9b92aba..13b502f 100644 --- a/headers/appsettings.h +++ b/headers/appsettings.h @@ -21,12 +21,6 @@ private: QSettings *settingsManager; // QSettings object which cares about our settings.ini file - QSettings *themeSettingsManager; - // QSettings object which cares about the themes - -signals: - void themeChanged(); - public slots: Q_INVOKABLE QString read(const QString &key); // function to read values from the settings file diff --git a/headers/bluerockbackend.h b/headers/bluerockbackend.h new file mode 100644 index 0000000..b9886e3 --- /dev/null +++ b/headers/bluerockbackend.h @@ -0,0 +1,81 @@ +/* + blueROCK - for digital rock + Copyright (C) 2019 Dorian Zedler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef SERVERCONN_H +#define SERVERCONN_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "QZXing.h" + +#ifdef Q_OS_ANDROID +#include +#elif defined Q_OS_IOS +#include "iospermissionutils.h" +#endif + +#include "shareUtils/shareutils.h" + +class BlueRockBackend : public QObject +{ + Q_OBJECT +public: + explicit BlueRockBackend(QObject *parent = nullptr); + +private: + QVariantMap _senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); + + ShareUtils* _shareUtils; +#ifdef Q_OS_IOS + IosPermissionUtils* _iosPermissionUtils; +#endif + const QStringList _validBaseDomains = {"digitalrock.de", "bluerock.dev"}; + bool _pendingIntentsChecked; + +signals: + Q_INVOKABLE void openedViaUrl(QString url, QString scheme); + +public slots: + + Q_INVOKABLE QVariant getWidgetData(QVariantMap params); + Q_INVOKABLE QVariantMap getParamsFromUrl(QString url); + Q_INVOKABLE void shareResultsAsUrl(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) + void onApplicationStateChanged(Qt::ApplicationState applicationState); +#endif + +}; + +#endif // SERVERCONN_H diff --git a/headers/iospermissionutils.h b/headers/iospermissionutils.h new file mode 100644 index 0000000..132e60f --- /dev/null +++ b/headers/iospermissionutils.h @@ -0,0 +1,22 @@ +#ifndef IOSPERMISSIONUTILS_H +#define IOSPERMISSIONUTILS_H + +#include +#include + +class IosPermissionUtils : QObject +{ + Q_OBJECT +public: + IosPermissionUtils(); + bool isCameraPermissionGranted(); + bool requestCameraPermission(); + +private: + QEventLoop* _responseWaitLoop; + +signals: + void permissionRequestFinished(bool result); +}; + +#endif // IOSPERMISSIONUTILS_H diff --git a/headers/serverconn.h b/headers/serverconn.h deleted file mode 100644 index 2630af3..0000000 --- a/headers/serverconn.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - blueROCK - for digital rock - Copyright (C) 2019 Dorian Zedler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#ifndef SERVERCONN_H -#define SERVERCONN_H - -#include -#include -#include -#include -#include - -class ServerConn : public QObject -{ - Q_OBJECT -public: - explicit ServerConn(QObject *parent = nullptr); - -private: - QVariantMap senddata(QUrl serviceUrl, QUrlQuery pdata = QUrlQuery()); - -signals: - -public slots: - - QVariant getWidgetData(QVariantMap params); - -}; - -#endif // SERVERCONN_H diff --git a/headers/shareUtils/androidshareutils.h b/headers/shareUtils/androidshareutils.h new file mode 100755 index 0000000..cbb30b0 --- /dev/null +++ b/headers/shareUtils/androidshareutils.h @@ -0,0 +1,51 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#ifndef ANDROIDSHAREUTILS_H +#define ANDROIDSHAREUTILS_H + +#include +#include + +#include "shareUtils/platformshareutils.h" + +class AndroidShareUtils : public PlatformShareUtils, public QAndroidActivityResultReceiver +{ + Q_OBJECT +public: + AndroidShareUtils(QObject* parent = nullptr); + bool checkMimeTypeView(const QString &mimeType) override; + bool checkMimeTypeEdit(const QString &mimeType) override; + virtual QString getTemporaryFileLocationPath() override; + void shareText(const QString &text, const QUrl &url) override; + void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + + void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) override; + void onActivityResult(int requestCode, int resultCode); + + void checkPendingIntents(const QString workingDirPath) override; + + static AndroidShareUtils* getInstance(); + +public slots: + void setFileUrlReceived(const QString &url); + void setOtherUrlReceived(const QString &url, const QString &scheme); + void setFileReceivedAndSaved(const QString &url); + bool checkFileExits(const QString &url); + +private: + bool mIsEditMode; + qint64 mLastModified; + QString mCurrentFilePath; + + static AndroidShareUtils* mInstance; + + void processActivityResult(int requestCode, int resultCode); + +}; + + +#endif // ANDROIDSHAREUTILS_H diff --git a/headers/shareUtils/ios/docviewcontroller.h b/headers/shareUtils/ios/docviewcontroller.h new file mode 100644 index 0000000..b70867b --- /dev/null +++ b/headers/shareUtils/ios/docviewcontroller.h @@ -0,0 +1,21 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#ifndef DOCVIEWCONTROLLER_HPP +#define DOCVIEWCONTROLLER_HPP + +#import +#import "iosshareutils.h" + +@interface DocViewController : UIViewController + +@property int requestId; + +@property IosShareUtils *mIosShareUtils; + +@end + + + +#endif // DOCVIEWCONTROLLER_HPP diff --git a/headers/shareUtils/ios/iosshareutils.h b/headers/shareUtils/ios/iosshareutils.h new file mode 100755 index 0000000..55d7bf7 --- /dev/null +++ b/headers/shareUtils/ios/iosshareutils.h @@ -0,0 +1,31 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#ifndef __IOSSHAREUTILS_H__ +#define __IOSSHAREUTILS_H__ + +#include "headers/shareUtils/platformshareutils.h" + +class IosShareUtils : public PlatformShareUtils +{ + Q_OBJECT + +public: + explicit IosShareUtils(QObject *parent = 0); + bool checkMimeTypeView(const QString &mimeType) override; + bool checkMimeTypeEdit(const QString &mimeType) override; + void shareText(const QString &text, const QUrl &url) override; + void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) override; + + void handleDocumentPreviewDone(const int &requestId); + +public slots: + void handleFileUrlReceived(const QUrl &url); + void handleHttpsUrlReceived(const QUrl &url); + +}; + +#endif diff --git a/headers/shareUtils/platformshareutils.h b/headers/shareUtils/platformshareutils.h new file mode 100644 index 0000000..99781e1 --- /dev/null +++ b/headers/shareUtils/platformshareutils.h @@ -0,0 +1,51 @@ +// (c) 2017 Ekkehard Gentz (ekke) +// this project is based on ideas from +// http://blog.lasconic.com/share-on-ios-and-android-using-qml/ +// see github project https://github.com/lasconic/ShareUtils-QML +// also inspired by: +// https://www.androidcode.ninja/android-share-intent-example/ +// https://www.calligra.org/blogs/sharing-with-qt-on-android/ +// https://stackoverflow.com/questions/7156932/open-file-in-another-app +// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData +// see also /COPYRIGHT and /LICENSE + +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#ifndef PLATFORMSHAREUTILS_H +#define PLATFORMSHAREUTILS_H + +#include +#include +#include +#include +#include + +class PlatformShareUtils : public QObject +{ + Q_OBJECT +signals: + void shareEditDone(int requestCode); + void shareFinished(int requestCode); + void shareNoAppAvailable(int requestCode); + void shareError(int requestCode, QString message); + void fileUrlReceived(QString url); + void otherUrlReceived(QString url, QString scheme); + void fileReceivedAndSaved(QString url); + +public: + PlatformShareUtils(QObject *parent = 0); + virtual ~PlatformShareUtils(); + virtual bool checkMimeTypeView(const QString &mimeType); + virtual bool checkMimeTypeEdit(const QString &mimeType); + virtual QString getTemporaryFileLocationPath(); + virtual void shareText(const QString &text, const QUrl &url); + virtual void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + virtual void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + virtual void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + + virtual void checkPendingIntents(const QString workingDirPath); +}; + +#endif // PLATFORMSHAREUTILS_H diff --git a/headers/shareUtils/shareutils.h b/headers/shareUtils/shareutils.h new file mode 100755 index 0000000..750ce73 --- /dev/null +++ b/headers/shareUtils/shareutils.h @@ -0,0 +1,63 @@ +// (c) 2017 Ekkehard Gentz (ekke) +// this project is based on ideas from +// http://blog.lasconic.com/share-on-ios-and-android-using-qml/ +// see github project https://github.com/lasconic/ShareUtils-QML +// also inspired by: +// https://www.androidcode.ninja/android-share-intent-example/ +// https://www.calligra.org/blogs/sharing-with-qt-on-android/ +// https://stackoverflow.com/questions/7156932/open-file-in-another-app +// http://www.qtcentre.org/threads/58668-How-to-use-QAndroidJniObject-for-intent-setData +// see also /COPYRIGHT and /LICENSE + +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#ifndef SHAREUTILS_H +#define SHAREUTILS_H + +#include +#include + +#include "shareUtils/platformshareutils.h" + +class ShareUtils : public QObject +{ + Q_OBJECT + + +signals: + void shareEditDone(int requestCode); + void shareFinished(int requestCode); + void shareNoAppAvailable(int requestCode); + void shareError(int requestCode, QString message); + void fileUrlReceived(QString url); + void otherUrlReceived(QString url, QString scheme); + void fileReceivedAndSaved(QString url); + +public slots: + void onShareEditDone(int requestCode); + void onShareFinished(int requestCode); + void onShareNoAppAvailable(int requestCode); + void onShareError(int requestCode, QString message); + void onFileUrlReceived(QString url); + void onOtherUrlReceived(QString url, QString scheme); + void onFileReceivedAndSaved(QString url); + +public: + explicit ShareUtils(QObject *parent = 0); + Q_INVOKABLE bool checkMimeTypeView(const QString &mimeType); + Q_INVOKABLE bool checkMimeTypeEdit(const QString &mimeType); + Q_INVOKABLE QString getTemporaryFileLocationPath(); + Q_INVOKABLE void shareText(const QString &text, const QUrl &url); + Q_INVOKABLE void sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + Q_INVOKABLE void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + Q_INVOKABLE void editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId); + Q_INVOKABLE void checkPendingIntents(const QString workingDirPath); + +private: + PlatformShareUtils* mPlatformShareUtils; + +}; + +#endif //SHAREUTILS_H diff --git a/ios/Info.plist b/ios/Info.plist new file mode 100644 index 0000000..b38762e --- /dev/null +++ b/ios/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + ${ASSETCATALOG_COMPILER_APPICON_NAME} + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${QMAKE_SHORT_VERSION} + CFBundleSignature + ${QMAKE_PKGINFO_TYPEINFO} + CFBundleVersion + ${QMAKE_FULL_VERSION} + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + MinimumOSVersion + ${IPHONEOS_DEPLOYMENT_TARGET} + NOTE + This file was generated by Qt/QMake. + NSCameraUsageDescription + blueROCK would like to access the camera. + UILaunchStoryboardName + LaunchScreen + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/ios/blueROCK.entitlements b/ios/blueROCK.entitlements new file mode 100644 index 0000000..9981ec4 --- /dev/null +++ b/ios/blueROCK.entitlements @@ -0,0 +1,11 @@ + + + + + com.apple.developer.associated-domains + + applinks:l.bluerock.dev + applinks:app.bluerock.dev + + + diff --git a/qzxing b/qzxing new file mode 160000 index 0000000..cfc7285 --- /dev/null +++ b/qzxing @@ -0,0 +1 @@ +Subproject commit cfc728583b867e157bd27e8b7c239c05a081e562 diff --git a/resources/qml/Components/AlignedButton.qml b/resources/qml/Components/AlignedButton.qml new file mode 100644 index 0000000..2d0a2ee --- /dev/null +++ b/resources/qml/Components/AlignedButton.qml @@ -0,0 +1,21 @@ +import QtQuick 2.0 +import QtQuick.Controls 2.12 +import QtQuick.Controls.Material 2.12 + +Button { + id: control + + property alias horizontalAlignment: label.horizontalAlignment + property alias verticalAlignment: label.verticalAlignment + + contentItem: Label { + id: label + + text: control.text + font: control.font + + color: !control.enabled ? control.Material.hintTextColor : + control.flat && control.highlighted ? control.Material.accentColor : + control.highlighted ? control.Material.primaryHighlightedTextColor : control.Material.foreground + } +} diff --git a/resources/qml/Components/AppToolBar.qml b/resources/qml/Components/AppToolBar.qml index e0bfa87..12b64bc 100644 --- a/resources/qml/Components/AppToolBar.qml +++ b/resources/qml/Components/AppToolBar.qml @@ -37,7 +37,7 @@ Item { Rectangle { id: toolBar - color: "white" + color: Material.background anchors.fill: parent Rectangle { diff --git a/resources/qml/Components/ColoredItemDelegate.qml b/resources/qml/Components/ColoredItemDelegate.qml new file mode 100644 index 0000000..9c5ccdf --- /dev/null +++ b/resources/qml/Components/ColoredItemDelegate.qml @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls 2 module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.15 +import QtQuick.Controls.Material.impl 2.15 + +ItemDelegate { + id: control + + Material.background: "transparent" + + background: Rectangle { + implicitHeight: control.Material.delegateHeight + + color: control.highlighted ? control.Material.listHighlightColor : control.Material.background + + Ripple { + width: parent.width + height: parent.height + + clip: visible + pressed: control.pressed + anchor: control + active: control.down || control.visualFocus || control.hovered + color: control.Material.rippleColor + } + } +} diff --git a/resources/qml/Components/CompetitionCalendarDelegate.qml b/resources/qml/Components/CompetitionCalendarDelegate.qml index 4d108ff..501cfed 100644 --- a/resources/qml/Components/CompetitionCalendarDelegate.qml +++ b/resources/qml/Components/CompetitionCalendarDelegate.qml @@ -1,8 +1,12 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.12 +import QtQuick.Templates 2.12 as T +import QtQuick.Controls.impl 2.12 +import QtQuick.Controls.Material.impl 2.12 -ItemDelegate { +ColoredItemDelegate { id: competitionDel property bool over @@ -81,15 +85,7 @@ ItemDelegate { NumberAnimation { target: competitionDel; property: "scale"; from: 1; to: 0.8; duration: 400 } } - Rectangle { - id: delBackroundRect - - anchors.fill: parent - - opacity: 0.5 - - color: control.getCompCatData(catId) === undefined ? "white":control.getCompCatData(catId)["bgcolor"] - } + Material.background: control.getCompCatData(catId) === undefined ? app.federalColor:control.getCompCatData(catId)["bgcolor"] Column { id: compDelCol @@ -117,14 +113,17 @@ ItemDelegate { ToolButton { id: bookmarkTb - icon.name: competitionDel.thisIsFavored ? "pinFilled":"pin" + + text: "\uf005" + font.family: competitionDel.thisIsFavored ? fa5solid.name : fa5regular.name + onClicked: { control.editFavorites(!competitionDel.thisIsFavored, parseInt(thisData['WetId'])) } Layout.alignment: Layout.Right - Behavior on icon.name { + Behavior on font.family { SequentialAnimation { NumberAnimation { property: "scale" @@ -146,8 +145,6 @@ ItemDelegate { Label { id: dateLa - color: "grey" - text: date } } diff --git a/resources/qml/Components/DataListView.qml b/resources/qml/Components/DataListView.qml index 38f7c7a..edc4270 100644 --- a/resources/qml/Components/DataListView.qml +++ b/resources/qml/Components/DataListView.qml @@ -64,21 +64,6 @@ ListView { } } - InfoArea { - id: infoArea - - anchors { - left: control.left - right: control.right - top: control.top - margins: app.landscape() ? control.width * 0.4:control.width * 0.3 - topMargin: control.height*( status === 901 ? 0.6:0.5) - height * 0.8 - } - - excludedCodes: [200, 902, 905] - errorCode: control.status - } - PullRefresher { target: control diff --git a/resources/qml/Components/DisclaimerDialog.qml b/resources/qml/Components/DisclaimerDialog.qml index c96c3cf..71b172c 100644 --- a/resources/qml/Components/DisclaimerDialog.qml +++ b/resources/qml/Components/DisclaimerDialog.qml @@ -13,8 +13,15 @@ Dialog { x: (parent.width - width) * 0.5 y: (parent.height - height) * 0.5 - width: parent.width * 0.8 - height: implicitHeight + implicitWidth: parent.width * 0.9 + + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, + contentHeight + topPadding + bottomPadding + + (implicitHeaderHeight > 0 ? implicitHeaderHeight + spacing : 0) + + (implicitFooterHeight > 0 ? implicitFooterHeight + spacing : 0)) + + //width: app.width * 0.8 + //height: implicitHeight modal: true @@ -41,8 +48,7 @@ Dialog { contentItem: Label { wrapMode: Text.Wrap - width: control.width * 0.8 - height: implicitHeight + width: control.parent * 0.8 text: control.content diff --git a/resources/qml/Components/FancyButton.qml b/resources/qml/Components/FancyButton.qml index 69b53ef..3ea03d0 100644 --- a/resources/qml/Components/FancyButton.qml +++ b/resources/qml/Components/FancyButton.qml @@ -19,8 +19,10 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtGraphicalEffects 1.0 +import QtQuick.Controls.Material 2.12 +import QtQuick.Controls.Material.impl 2.12 -ToolButton { +MouseArea { id: control property string image @@ -39,7 +41,7 @@ ToolButton { } } - contentItem: Item { + Item { id: controlBackgroundContainer anchors.fill: parent @@ -67,7 +69,7 @@ ToolButton { radius: height * 0.2 - color: control.down ? Qt.darker(control.backgroundColor, 1.03) : control.backgroundColor + color: Material.dialogColor //control.down ? Qt.darker(control.backgroundColor, 1.03) : control.backgroundColor Image { id: buttonIcon @@ -84,22 +86,30 @@ ToolButton { scale: control.imageScale } - Behavior on color { - ColorAnimation { - duration: 100 + Ripple { + id: ripple + visible: true + clipRadius: controlBackground.radius + clip: true + width: parent.width + height: parent.height + pressed: control.pressed + anchor: control + active: control.pressed || control.visualFocus || control.containsMouse + color: control.Material.rippleColor + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Item { + width: ripple.width + height: ripple.height + Rectangle { + anchors.fill: parent + radius: controlBackground.radius + } + } } } } } - - Text { - id: conetntText - text: qsTr(control.text) - anchors.centerIn: parent - font: parent.font - color: control.textColor - opacity: control.enabled ? 1:0.4 - } - - } diff --git a/resources/qml/Components/MovingLabel.qml b/resources/qml/Components/MovingLabel.qml new file mode 100644 index 0000000..67b1ad8 --- /dev/null +++ b/resources/qml/Components/MovingLabel.qml @@ -0,0 +1,90 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.12 + +Item { + id: control + + property alias text: firstLabel.text + property alias font: firstLabel.font + property alias verticalAlignment: firstLabel.verticalAlignment + property int spacing: 30 + + property MovingLabel syncWithLabel + property int _spacing: syncWithLabel && syncWithLabel._labelWidth > _labelWidth ? (syncWithLabel._labelWidth - _labelWidth + syncWithLabel.spacing) : spacing + property alias _labelWidth: firstLabel.width + + signal linkActivated(string link) + + clip: true + height: firstLabel.height + + onTextChanged: { + _resetScroll() + if(control.syncWithLabel) + control.syncWithLabel._resetScroll() + } + + function startScroll(triggerSyncedLabel=true) { + if(control.syncWithLabel && triggerSyncedLabel) + control.syncWithLabel.startScroll(false) + if(control.width < firstLabel.width && !scrollAnimation.running) + scrollAnimation.start() + } + + function _resetScroll() { + scrollAnimation.stop() + firstLabel.anchors.leftMargin = 0 + } + + Label { + id: firstLabel + + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + } + + onLinkActivated: control.onLinkActivated(link) + } + + Label { + id: secondLabel + + anchors { + left: firstLabel.right + leftMargin: control._spacing + verticalCenter: firstLabel.verticalCenter + } + + visible: scrollAnimation.running + + font: firstLabel.font + text: firstLabel.text + verticalAlignment: firstLabel.verticalAlignment + elide: firstLabel.elide + bottomPadding: firstLabel.bottomPadding + padding: firstLabel.padding + + onLinkActivated: control.onLinkActivated(link) + } + + MouseArea { + anchors.fill: parent + onClicked: control.startScroll() + } + + NumberAnimation { + id: scrollAnimation + target: firstLabel + property: "anchors.leftMargin" + from: 0 + to: -(firstLabel.width + control._spacing) + duration: (to / -100) * 1500 + + onRunningChanged: { + if(!running) + control._resetScroll() + } + } +} diff --git a/resources/qml/Components/PullRefresher.qml b/resources/qml/Components/PullRefresher.qml index 9004143..781106a 100644 --- a/resources/qml/Components/PullRefresher.qml +++ b/resources/qml/Components/PullRefresher.qml @@ -18,6 +18,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 +import QtQuick.Controls.Material 2.12 import QtGraphicalEffects 1.0 Item { @@ -37,10 +38,6 @@ Item { property double dragRefreshPositionMultiplier: 0.6 // position of the item when starting to refresh - property color backgroundColor: "white" // color for the pre-defined background - property color pullIndicatorColor: "black" // color for the pre-defined pull indicator - //property color busyIndicatorColor: "pink" // color for the pre-defined busy indicator - readonly property double dragProgress: Math.min( userPosition / dragOutPosition, 1) property Component background: Item { @@ -61,7 +58,7 @@ Item { anchors.fill: parent radius: width * 0.5 - color: control.backgroundColor + color: Material.dialogColor } } property Component busyIndicator: BusyIndicator { running: true } @@ -107,7 +104,7 @@ Item { ctx.reset() ctx.lineWidth = lineWidth; - ctx.strokeStyle = control.pullIndicatorColor; + ctx.strokeStyle = control.Material.foreground; // middle line ctx.moveTo(width/2, topMargin); diff --git a/resources/qml/Components/ResultDelegate.qml b/resources/qml/Components/ResultDelegate.qml index 29a1df5..e0d4df3 100644 --- a/resources/qml/Components/ResultDelegate.qml +++ b/resources/qml/Components/ResultDelegate.qml @@ -1,12 +1,13 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 -ItemDelegate { +ColoredItemDelegate { id: partDel - property int ind: index - property var thisData: widgetData[ "participants" ][partDel.ind] + property int thisIndex: index + property var thisData: widgetData[ "participants" ][partDel.thisIndex] width: control.width height: partDelCol.showSideBySide ? 40:70 @@ -24,23 +25,14 @@ ItemDelegate { app.openWidget({person:thisData["PerId"]}) } + highlighted: partDel.thisIndex % 2 == 0 + ParallelAnimation { id: fadeInPa NumberAnimation { target: partDel; property: "opacity"; from: 0; to: 1.0; duration: 400 } NumberAnimation { target: partDel; property: "scale"; from: 0.8; to: 1.0; duration: 400 } } - Rectangle { - id: partDelBackgroundRect - anchors.fill: parent - - width: partDel.width - - color: partDel.ind % 2 == 0 ? "white":"lightgrey" - - opacity: 0.2 - } - GridLayout { id: partDelCol @@ -133,8 +125,8 @@ ItemDelegate { function getDataForIcon(index){ // TODO: clean - var resultString = widgetData[ "participants" ][partDel.ind]["boulder"+(index+1)] - var numTrys = widgetData[ "participants" ][partDel.ind]["try"+(index+1)] + var resultString = widgetData[ "participants" ][partDel.thisIndex]["boulder"+(index+1)] + var numTrys = widgetData[ "participants" ][partDel.thisIndex]["try"+(index+1)] var resultList = [] @@ -222,7 +214,7 @@ ItemDelegate { context.arc(radius + offsetX, radius + offsetY, radius, Math.PI, 1.5 * Math.PI); // fill - if(resultData[0] !== -1) { + if(resultData[0] !== -1 || resultData[2] !== -1) { // if there is a result available -> draw background context.fillStyle = "#b7b7b7"; } @@ -234,7 +226,7 @@ ItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); if(resultData[1] > 0){ @@ -262,7 +254,7 @@ ItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); @@ -287,7 +279,7 @@ ItemDelegate { // outline context.lineWidth = 1; - context.strokeStyle = '#424242'; + context.strokeStyle = Material.primaryTextColor; context.stroke(); } } @@ -311,6 +303,8 @@ ItemDelegate { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + color: "#dd000000" + text: boulderResCv.resultData[2] } @@ -336,6 +330,8 @@ ItemDelegate { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + color: "#dd000000" + text: boulderResCv.resultData[1] } @@ -360,6 +356,8 @@ ItemDelegate { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter + color: "#dd000000" + text: boulderResCv.resultData[0] } } @@ -427,7 +425,7 @@ ItemDelegate { visible: index === 0 - color: "grey" + color: Material.primaryTextColor } Rectangle { @@ -438,7 +436,7 @@ ItemDelegate { width: 1 height: parent.height - color: "grey" + color: Material.primaryTextColor } Label { @@ -454,7 +452,9 @@ ItemDelegate { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - text: widgetData[ "participants" ][partDel.ind]["result"+(generalResRep.routes[index][0])] === undefined ? "":widgetData[ "participants" ][partDel.ind]["result"+(generalResRep.routes[index][0])] + text: widgetData["participants"][partDel.thisIndex]["result"+(generalResRep.routes[index][0])] === undefined ? + "": + widgetData[ "participants" ][partDel.thisIndex]["result"+(generalResRep.routes[index][0])] } } @@ -479,7 +479,7 @@ ItemDelegate { font.pixelSize: Math.abs( height * 0.6 ) minimumPixelSize: 1 - text: widgetData[ "participants" ][partDel.ind]["result"] === undefined ? "":widgetData[ "participants" ][partDel.ind]["result"] + text: widgetData[ "participants" ][partDel.thisIndex]["result"] === undefined ? "":widgetData[ "participants" ][partDel.thisIndex]["result"] } } } diff --git a/resources/qml/Components/SelectorPopup.qml b/resources/qml/Components/SelectorPopup.qml index f9dcd4d..356441e 100644 --- a/resources/qml/Components/SelectorPopup.qml +++ b/resources/qml/Components/SelectorPopup.qml @@ -1,5 +1,5 @@ -import QtQuick 2.9 -import QtQuick.Controls 2.4 +import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Controls.Material 2.3 Dialog { @@ -7,9 +7,9 @@ Dialog { property var dataObj property string subTitle: "" - property int implicitY: parent.height - implicitHeight signal selectionFinished(int index, var data) + signal linkActivated(string link) parent: Overlay.overlay @@ -33,38 +33,35 @@ Dialog { id: selectorPuHeaderCol width: control.width - height: headerSubLa.text !== "" && headerLa.text !== "" ? 73 : 40 + height: headerLa.height + headerTopSpacerItm.height + (control.subTitle ? headerSubLa.height:0) - Label { + Item { + id: headerTopSpacerItm + height: control.padding + width: parent.width + } + + MovingLabel { id: headerLa - visible: control.title + anchors.horizontalCenter: parent.horizontalCenter + width: selectorPuHeaderCol.width - control.padding * 2 - width: selectorPuHeaderCol.width - - elide: "ElideRight" - padding: control.padding - bottomPadding: 0 font.bold: true font.pixelSize: 16 text: control.title - onLinkActivated: { - console.log("Opening " + link) - Qt.openUrlExternally(link) - } + onLinkActivated: control.linkActivated(link) } Label { id: headerSubLa - visible: control.subTitle + anchors.horizontalCenter: parent.horizontalCenter + width: selectorPuHeaderCol.width - control.padding * 2 - width: selectorPuHeaderCol.width - - elide: "ElideRight" - padding: control.padding + wrapMode: Text.Wrap topPadding: 5 bottomPadding: 0 font.bold: true @@ -73,8 +70,7 @@ Dialog { text: control.subTitle onLinkActivated: { - console.log("Opening " + link) - Qt.openUrlExternally(link) + control.linkActivated(link) } } @@ -83,6 +79,7 @@ Dialog { background: Item { Rectangle { id: backgroundRect + anchors { fill: parent bottomMargin: -radius diff --git a/resources/qml/Components/SharePopup.qml b/resources/qml/Components/SharePopup.qml new file mode 100644 index 0000000..4a512df --- /dev/null +++ b/resources/qml/Components/SharePopup.qml @@ -0,0 +1,124 @@ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QZXing 3.1 +import QtGraphicalEffects 1.0 + +Dialog { + id: control + + property string _shareUrl + property string _compName + + parent: Overlay.overlay + + x: (parent.width - width) * 0.5 + y: (parent.height - height) * 0.5 + + modal: true + //% "Share these results" + title: qsTrId("#shareResultsHeadline") + + onClosed: { + shareComponentLoader.sourceComponent = null + } + + 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: [ + //% "Link" + ["\uf0c1", qsTrId("#shareByLink"), serverConn.shareResultsAsUrl], + //% "QR-Code" + ["\uf029", qsTrId("#shareByQrCode"), function() { + stackLayout.currentIndex = 1 + } + ], + //% "Poster" + ["\uf1c1", qsTrId("#shareByPoster"), 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 finalSize: app.landscape() ? app.height * 0.8 : app.width * 0.8 + property int size: stackLayout.currentIndex === 1 ? finalSize:menuRow.height + + Layout.preferredHeight: size + Layout.preferredWidth: size + + sourceSize.width: finalSize + sourceSize.height: finalSize + + fillMode: Image.PreserveAspectFit + + source: "image://QZXing/encode/" + _shareUrl + "?border=true&correctionLevel=H" + + RectangularGlow { + id: effect + anchors.fill: blurRockLogoBackgroundRect + glowRadius: 0 + spread: 0 + opacity: 0.8 + color: "black" + cornerRadius: blurRockLogoBackgroundRect.radius + } + + Rectangle { + id: blurRockLogoBackgroundRect + anchors.centerIn: parent + width: parent.width * 0.25 + height: width + radius: height * 0.2 + color: "white" + Image { + anchors.centerIn: parent + width: parent.width * 0.8 + height: width + mipmap: true + source: "qrc:/icons/blueRockHold.png" + } + } + + Behavior on size { + NumberAnimation { + duration: 200 + } + } + } + } + } + + function appear(shareUrl, compName) { + _shareUrl = shareUrl + _compName = compName + shareComponentLoader.sourceComponent = shareComponent + control.open() + } +} diff --git a/resources/qml/Components/SpeedFlowChart.qml b/resources/qml/Components/SpeedFlowChart.qml index 520852b..26705fd 100644 --- a/resources/qml/Components/SpeedFlowChart.qml +++ b/resources/qml/Components/SpeedFlowChart.qml @@ -385,7 +385,7 @@ Item { width: parent.width height: roundItem.tileSize //scale: 0.9 - color: "white" + color: Material.dialogColor border.color: "lightgrey" border.width: 0 radius: height * 0.2 @@ -438,7 +438,7 @@ Item { font.bold: matchItm.winnerIndex === index elide: "ElideRight" - color: matchItm.winnerIndex === index ? "green":"black" + color: matchItm.winnerIndex === index ? Material.color(Material.Green):Material.primaryTextColor text: matchItm.thisMatchData[index] !== undefined ? matchItm.thisMatchData[index]['firstname'] + " " + matchItm.thisMatchData[index]['lastname'] :"-" } diff --git a/resources/qml/Components/SpeedFlowChartLocker.qml b/resources/qml/Components/SpeedFlowChartLocker.qml index 68d2984..f636caa 100644 --- a/resources/qml/Components/SpeedFlowChartLocker.qml +++ b/resources/qml/Components/SpeedFlowChartLocker.qml @@ -1,21 +1,20 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.12 import QtQuick.Layouts 1.15 import QtPurchasing 1.12 -Rectangle { - id: speedFlowChartLockedOverlay +Page { + id: control anchors.fill: parent - anchors.margins: -20 - - color: "white" Connections { target: speedFlowChartProduct function onPurchaseFailed() { - purchaseBt.text = qsTr("Purchase failed") + //% "Purchase failed" + purchaseBt.text = qsTrId("#purchaseFailed") purchaseBt.enabled = false buttonTextResetTimer.start() } @@ -28,62 +27,48 @@ Rectangle { repeat: false onTriggered: { purchaseBt.text = (speedFlowChartProduct.status === Product.Registered - ? "Buy now for " + speedFlowChartProduct.price - : qsTr("this item is currently unavailable")) + //% "Buy now for" + ? qsTrId("#buyNowFor") + " " + speedFlowChartProduct.price + //% "This item is currently unavailable" + : qsTrId("#itemIsUnavailable")) purchaseBt.enabled = true } } - ColumnLayout { + Column { id: lockedLayout anchors { fill: parent - topMargin: parent.height * 0.05 + topMargin: parent.height * 0.02 bottomMargin: parent.height * 0.05 rightMargin: parent.width * 0.1 + 20 leftMargin: parent.width * 0.1 + 20 } - //spacing: parent.height * 0.05 + spacing: lockedLayout.height * 0.01 - Image { - id: name - - Layout.alignment: Layout.Center - Layout.preferredHeight: height - Layout.preferredWidth: width - - width: lockedLayout.height * 0.05 - height: width - - mipmap: true - - source: "qrc:/icons/lock.png" - } - - Text { - Layout.fillWidth: true - - height: parent.height * 0.05 - - text: qsTr("This is a premium feature.") + Label { + anchors.horizontalCenter: parent.horizontalCenter + height: lockedLayout.height * 0.03 + width: lockedLayout.width + //% "This is a premium feature." + text: qsTrId("#thisIsAPremiumFeature") + font.pixelSize: height font.bold: true - font.pixelSize: parent.height * 0.05 fontSizeMode: Text.Fit + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter minimumPixelSize: 1 - - horizontalAlignment: Text.AlignHCenter - } SwipeGallery { property string platformIcons: Qt.platform.os === "osx" || Qt.platform.os === "iso" ? "ios":"android" - Layout.fillHeight: true - Layout.fillWidth: true + height: lockedLayout.height * 0.85 + width: lockedLayout.width images: [ "qrc:/screenshots/SpeedFlowchartDemo/" + platformIcons + "/1.jpeg", @@ -94,39 +79,52 @@ Rectangle { Button { id: purchaseBt - Layout.alignment: Layout.Center + anchors.horizontalCenter: parent.horizontalCenter + height: lockedLayout.height * 0.07 enabled: speedFlowChartProduct.status === Product.Registered text: speedFlowChartProduct.status === Product.Registered - ? "Buy now for " + speedFlowChartProduct.price - : qsTr("This item is currently unavailable") - icon.name: "buy" + ? "\uf218 "+ qsTrId("#buyNowFor") + " " + speedFlowChartProduct.price + : qsTrId("#itemIsUnavailable") + font.family: fa5solid.name + font.pixelSize: height * 0.4 + font.capitalization: Font.MixedCase onClicked: speedFlowChartProduct.purchase() } RowLayout { + id: bottomRow - Layout.alignment: Layout.Center + anchors.horizontalCenter: parent.horizontalCenter + height: lockedLayout.height * 0.065 - Button { - id: restorePurchaseButton - Layout.alignment: Layout.Center - visible: speedFlowChartProduct.status === Product.Registered + Button { + id: restorePurchaseButton + Layout.alignment: Layout.Center + Layout.preferredHeight: bottomRow.height + //visible: speedFlowChartProduct.status === Product.Registered - flat: true - text: "restore purchase" + flat: true + font.pixelSize: height * 0.4 + font.capitalization: Font.MixedCase + //% "Restore purchase" + text: qsTrId("#restorePurchase") - onClicked: inAppProductStore.restorePurchases() - } + onClicked: inAppProductStore.restorePurchases() + } - Button { - id: contactSupportButton - Layout.alignment: Layout.Center + Button { + id: contactSupportButton + Layout.alignment: Layout.Center + Layout.preferredHeight: bottomRow.height - flat: true - text: "contact support" + flat: true + font.pixelSize: height * 0.4 + font.capitalization: Font.MixedCase + //% "contact support" + text: qsTrId("#contact support") - onClicked: Qt.openUrlExternally("mailto:contact@itsblue.de") - } + onClicked: Qt.openUrlExternally("mailto:contact@itsblue.de") + } } } diff --git a/resources/qml/Components/SpeedFlowChartPopup.qml b/resources/qml/Components/SpeedFlowChartPopup.qml index 5456a8d..3200d82 100644 --- a/resources/qml/Components/SpeedFlowChartPopup.qml +++ b/resources/qml/Components/SpeedFlowChartPopup.qml @@ -11,6 +11,9 @@ Rectangle { //property bool unlocked: QT_DEBUG || appSettings.read("speedBackendPurchase") === "1" property bool unlocked: appSettings.read("speedBackendPurchase") === "1" + Component.onCompleted: { + console.warn("unlocked:", appSettings.read("speedBackendPurchase")) + } state: "hidden" diff --git a/resources/qml/Components/SwipeGallery.qml b/resources/qml/Components/SwipeGallery.qml index d159c5c..ea07b81 100644 --- a/resources/qml/Components/SwipeGallery.qml +++ b/resources/qml/Components/SwipeGallery.qml @@ -14,6 +14,7 @@ Item { anchors.fill: parent anchors.margins: 1 anchors.bottomMargin: indicator.height + spacing: width * 0.2 Repeater { model: control.images.length diff --git a/resources/qml/Pages/QrCodeScanPage.qml b/resources/qml/Pages/QrCodeScanPage.qml new file mode 100644 index 0000000..7474d1c --- /dev/null +++ b/resources/qml/Pages/QrCodeScanPage.qml @@ -0,0 +1,298 @@ +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 + +import "../Components" + +Page { + id: control + + property string _statusText: "" + property string _statusColor: Material.primaryTextColor + property bool _freezeScanning: false + signal headerComponentChanged() + + //% "Scan QR-Code" + title: qsTrId("#scanQrCode") + + onFocusChanged: focus ? open() : close() + + function open() { + _setDefaultStatusText() + control._freezeScanning = false + if(serverConn.isCameraPermissionGranted()) + cameraLoader.sourceComponent = cameraComponent + else + cameraLoader.sourceComponent = noPermissionComponent + } + + function close() { + cameraLoader.sourceComponent = null + } + + function _setDefaultStatusText() { + //% "Place the Code in the center" + _statusText = qsTrId("#placeQrCodeInCenter") + _statusColor = Material.primaryTextColor + } + + function _handleTag(tag) { + if(control._freezeScanning) + return + + control._freezeScanning = true + + //% "Plase wait" + control._statusText = qsTrId("#pleaseWait") + "..." + + if(app.openWidgetFromUrl(tag)) + control.close() + else { + //% "Invalid QR-Code" + control._statusText = qsTrId("#invalidQrCode") + control._statusColor = Material.color(Material.Red) + statusTextResetTimer.start() + control._freezeScanning = false + } + } + + function _requestCameraPermission() { + var permissionGranted = serverConn.requestCameraPermission() + + if(permissionGranted) + cameraLoader.sourceComponent = cameraComponent + } + + contentItem: Loader { + id: cameraLoader + + anchors.fill: parent + + //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 + } + } + + FancyBusyIndicator { + anchors.centerIn: parent + } + + 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.5 + 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 + } + } + } + } + + Component { + id: noPermissionComponent + ColumnLayout { + //anchors.fill: parent + //anchors.margins: app.landscape() ? app.height * 0.1 : app.width * 0.1 + + property int columnWidth: control.width * 0.9 + + spacing: height * 0.02 + + Item { + Layout.fillHeight: true + } + + Label { + id: noPermissionIcon + + Layout.preferredWidth: parent.columnWidth + Layout.alignment: Layout.Center + + font.pixelSize: app.landscape() ? parent.height * 0.25:parent.height * 0.15 + font.family: fa5solid.name + + horizontalAlignment: Text.AlignHCenter + + text: "\uf3ed" + } + Label { + id: noPermissionText + + Layout.preferredWidth: parent.columnWidth + Layout.alignment: Layout.Center + + font.bold: true + font.pixelSize: noPermissionIcon.height * 0.2 + + horizontalAlignment: Text.AlignHCenter + + wrapMode: Text.Wrap + + //% "Camera access required" + text: qsTrId("#cameraPermissionDenied") + } + + Label { + id: noPermissionDetailText + + Layout.preferredWidth: parent.columnWidth + Layout.alignment: Layout.Center + + font.pixelSize: noPermissionText.font.pixelSize * 0.7 + + horizontalAlignment: Text.AlignHCenter + + wrapMode: Text.Wrap + + //% "blueROCK needs to access your camera in order to scan QR-Codes. It will never record or store any photos or videos." + text: qsTrId("#cameraPermissionDeniedDetails") + } + + Button { + id: grantPermissionButton + + Layout.alignment: Layout.Center + + //% "Allow access" + text: qsTrId("#allowAccess") + + onClicked: control._requestCameraPermission() + } + + + Item { + Layout.fillHeight: true + } + + } + } + + QZXingFilter { + id: zxingFilter + + decoder { + onTagFound: control._handleTag(tag) + + enabledDecoders: QZXing.DecoderFormat_QR_CODE + } + } + + Timer { + id: statusTextResetTimer + running: false + repeat: false + interval: 3000 + onTriggered: _setDefaultStatusText() + } +} diff --git a/resources/qml/Pages/StartPage.qml b/resources/qml/Pages/StartPage.qml index 6289ebb..72dde86 100644 --- a/resources/qml/Pages/StartPage.qml +++ b/resources/qml/Pages/StartPage.qml @@ -19,6 +19,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtQuick.Layouts 1.0 +import QtQuick.Controls.Material 2.12 import "../Components" @@ -37,11 +38,10 @@ Page { topMargin: root.height * 0.03 } - height: menuGr.buttonSize * 0.3 - spacing: anchors.topMargin * 0.5 + height: app.landscape() ? menuGr.buttonSize * 0.2:menuGr.buttonSize * 0.3 } - Grid { + GridLayout { id: menuGr anchors.centerIn: parent @@ -49,17 +49,19 @@ Page { rows: app.landscape() ? 1:2 columns: app.landscape() ? 2:1 - spacing: !app.landscape() ? parent.height * 0.08:parent.width * 0.1 + rowSpacing: app.landscape() ? parent.width * 0.1:headerBadge.anchors.topMargin + columnSpacing: rowSpacing - property int buttonSize: app.landscape() ? parent.width * 0.2:parent.height * 0.2 + property int buttonSize: app.landscape() ? parent.width * 0.2:parent.height * 0.19 FancyButton { id: davBt - height: menuGr.buttonSize - width: height + Layout.preferredHeight: menuGr.buttonSize + Layout.preferredWidth: menuGr.buttonSize + Layout.alignment: Layout.Center - image: "qrc:/icons/dav.png" + image: Material.theme === Material.Dark ? "qrc:/icons/dav-dark.png":"qrc:/icons/dav.png" onClicked: { app.openWidget({nation:"GER"}) @@ -70,90 +72,107 @@ Page { FancyButton { id: sacBt - height: menuGr.buttonSize - width: height + Layout.preferredHeight: menuGr.buttonSize + Layout.preferredWidth: menuGr.buttonSize + Layout.alignment: Layout.Center - image: "qrc:/icons/sac.png" + image: Material.theme === Material.Dark ? "qrc:/icons/sac-dark.png":"qrc:/icons/sac.png" onClicked: { app.openWidget({nation:"SUI"}) } - - } - - } - - RowLayout { - anchors { - horizontalCenter: parent.horizontalCenter - bottom: bottomDigitalrockDisclaimerLabel.top - } - - Button { - id: ifscDisclaimerButton - - flat: true - font.bold: true - font.pixelSize: aboutBluerockDisclaimerButton.font.pixelSize - - text: "Where are the IFSC results?" - - onClicked: ifscDisclaimerDialog.open() - } - - Button { - id: aboutBluerockDisclaimerButton - - flat: true - font.pixelSize: bottomDigitalrockDisclaimerLabel.paintedHeight * (Qt.platform.os === "android" ? 0.8:0.735) - - text: "About blueROCK" - - onClicked: aboutBluerockDisclaimerDialog.open() } } - Label { - id: bottomDigitalrockDisclaimerLabel + Grid { + id: footerMenu + anchors { - horizontalCenter: parent.horizontalCenter bottom: parent.bottom - bottomMargin: headerBadge.anchors.topMargin + margins: headerBadge.anchors.topMargin + horizontalCenter: parent.horizontalCenter } - width: parent.width * 0.9 - height: anchors.bottomMargin + width: app.landscape() ? childrenRect.width : parent.width * 0.8 + height: app.landscape() ? headerBadge.height : Math.min(headerBadge.height * 2, width * 0.27) - fontSizeMode: Label.Fit - minimumPixelSize: 1 + columnSpacing: height * 0.1 - horizontalAlignment: Text.AlignHCenter + columns: app.landscape() ? 4:2 + rows: app.landscape() ? 1:2 - text: "Resultservice and rankings provided by digital ROCK." + Repeater { + id: buttonRepeater + property var buttons: [ + //% "IFSC results" + ["\uf059", qsTrId("#ifscResults"), ifscDisclaimerDialog.open], + [ + "\uf042", + Material.theme === Material.Light ? + //% "Dark mode" + qsTrId("#darkMode"): + //% "Light mode" + qsTrId("#lightMode"), + app.toggleDarkMode + ], + //% "About blueROCK" + ["\uf05a", qsTrId("#aboutBluerock"), aboutBluerockDisclaimerDialog.open], + ["\uf029", qsTrId("#scanQrCode"), function(){ + mainStack.push("qrc:/Pages/QrCodeScanPage.qml") + }], + ] - onLinkActivated: { - Qt.openUrlExternally(link) + model: buttons + + delegate: Item { + + 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 { + right: isLeft && !app.landscape() ? parent.right : undefined + left: isLeft && !app.landscape() ? undefined : parent.left + centerIn: app.landscape() ? parent : undefined + } + + height: parent.height + + flat: true + + font.family: fa5solid.name + font.pixelSize: height * 0.4 + font.capitalization: Font.MixedCase + //horizontalAlignment: isLeft ? Text.AlignRight : Text.AlignLeft + + text: isLeft && !app.landscape() ? modelData[1] + " " + modelData[0] : modelData[0] + " " + modelData[1] + + onClicked: buttonRepeater.buttons[index][2]() + } + } } } DisclaimerDialog { id: ifscDisclaimerDialog - title: "Where are the IFSC results?" - content: "Unfortunately, the IFSC has restricted the access to their data and is not willing to share results with blueROCK anymore. " + - "Because of this, blueROCK is no longer able to access and display IFSC results.

" + - "You can find current IFSC results over here and on their website." + Material.theme: root.Material.theme + //% "Where are the IFSC results?" + title: qsTrId("#ifscDisclaimerTitle") + //% "Unfortunately, the IFSC has restricted the access to their data and is not willing to share results with blueROCK anymore. Because of this, blueROCK is no longer able to access and display IFSC results.

You can find current IFSC results over here and on their website." + content: qsTrId("#ifscDisclaimer") } DisclaimerDialog { id: aboutBluerockDisclaimerDialog - title: "blueROCK v" + APP_VERSION + "
By Itsblue Development" - content: "This app was built using the Qt Framework " + - "licensed under the GNU lgplV3 license.

"+ - - "This app is open source and licensed under the GNU agplV3 license," + - "the source code can be found here." - + Material.theme: root.Material.theme + //% "privacy policy" + title: "blueROCK v" + APP_VERSION + "
By Itsblue Development, " + qsTrId("#privacyPolicy") + "" + //% "This app was built using the Qt Framework licensed under the GNU lgplV3 license.

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." + content: qsTrId("#aboutBluerockDisclaimer") } - } diff --git a/resources/qml/Pages/WidgetPage.qml b/resources/qml/Pages/WidgetPage.qml index 5f01e5b..9a5a68a 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 { @@ -88,7 +93,13 @@ Page { root.ready = false } } - else if(ret["status"] === 404 && [WidgetPage.WidgetType.Registration, WidgetPage.WidgetType.Startlist, WidgetPage.WidgetType.Result].includes(root.widgetType) && root.params["route"] !== "") { + else if(ret["status"] === 404 && + [ + WidgetPage.WidgetType.Registration, + WidgetPage.WidgetType.Startlist, + WidgetPage.WidgetType.Result + ].includes(root.widgetType) && + root.params["route"] !== "") { // if we get a 404 and have startlist, results or registration, the route was not found -> remove round and try again root.params["route"] = "" loadData(root.params) @@ -118,6 +129,10 @@ Page { } + function areParamsValid() { + + } + function checkWidgetType(params, widgetData){ var widgetType @@ -164,10 +179,25 @@ Page { // aggregated widgetType = WidgetPage.WidgetType.Aggregated } + else { + widgetType = WidgetPage.WidgetType.Invalid + } return widgetType } + function encodeQueryData(data) { + const ret = []; + for (let d in data) + ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d])); + return ret.join('&'); + } + + function shareWidget(compName) { + var url = "https://l.bluerock.dev/?" + encodeQueryData(params) + sharePu.appear(url, compName) + } + Loader { id: widgetLd @@ -201,9 +231,6 @@ Page { delete(widgetLd.sourceComponent) return false } - - - // } function getFile(widgetType) { @@ -248,11 +275,14 @@ Page { SelectorPopup { id: selectorPu + Material.theme: root.Material.theme + contentItem: ListView { id: selectorLv property int delegateHeight: 50 spacing: 10 + clip: true implicitHeight: model === 0 ? 0:(delegateHeight + spacing) * model @@ -267,7 +297,6 @@ Page { leftMargin: 3 bottom: selectorLv.bottom } - } delegate: Button { @@ -287,4 +316,10 @@ Page { } } } + + SharePopup { + id: sharePu + + Material.theme: root.Material.theme + } } diff --git a/resources/qml/Widgets/CalendarWidget.qml b/resources/qml/Widgets/CalendarWidget.qml index f9785c1..03ae164 100644 --- a/resources/qml/Widgets/CalendarWidget.qml +++ b/resources/qml/Widgets/CalendarWidget.qml @@ -19,6 +19,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtQuick.Layouts 1.3 +import QtQuick.Controls.Material 2.12 import "../Components" @@ -27,7 +28,8 @@ DataListView { property bool ready - property string title: (params.nation === "ICC" ? "IFSC":params.nation === "GER" ? "DAV":"SAC") + " " + qsTr("calendar") + " " + control.year + //% "calendar" + property string title: (params.nation === "ICC" ? "IFSC":params.nation === "GER" ? "DAV":"SAC") + " " + qsTrId("#calendar") + " " + control.year property Component headerComponent: RowLayout { @@ -41,7 +43,9 @@ DataListView { control.changeYear() } - icon.name: "year" + text: "\uf133" + font.family: fa5solid.name + } ToolButton { @@ -60,16 +64,19 @@ DataListView { } } - compCats.push( {"text": qsTr("Pinned"), "data": {"sort_rank":0, "cat_id":[-1]}} ) + //% "Favorites" + compCats.push( {"text": qsTrId("#favorites"), "data": {"sort_rank":0, "cat_id":[-1]}} ) compCats.sort(function(a, b) { return a['data']['sort_rank'] - b['data']['sort_rank']; }); - filterSelectPu.appear(compCats, qsTr("Select Filters"), "") + //% "Select filters" + filterSelectPu.appear(compCats, qsTrId("#selectFilters"), "") } - icon.name: "filter" + text: "\uf0b0" + font.family: fa5solid.name } ToolButton { @@ -79,7 +86,8 @@ DataListView { control.openCup() } - icon.name: "cup" + text: "\uf091" + font.family: fa5solid.name } } @@ -105,10 +113,15 @@ DataListView { initFilters() initFavorites() - if(model){ + if(model && widgetData["competitions"].length > 0){ control.status = 200 control.ready = true } + else if(widgetData["competitions"].length === 0) { + control.status = 404 + control.ready = false + } + else { control.ready = false control.status = 901 @@ -192,13 +205,18 @@ DataListView { var infoUrls = getCompInfoUrls(compIndex) var infosheet = ""; if(infoUrls.length >= 1) - infosheet += ("" + qsTr('infosheet') + "") + //% "Infosheet" + infosheet += ("" + qsTrId("#infosheet") + "") if(infoUrls.length === 2) - infosheet += (", " + qsTr('further infos') + "") + //% "Further infos" + infosheet += (", " + qsTrId("#furtherInfos") + "") console.log("Infosheet: " + infosheet) - var eventWebsite = control.widgetData["competitions"][compIndex]["homepage"] !== undefined ? ("" + qsTr('Event Website') + ""):"" + + var eventWebsite = control.widgetData["competitions"][compIndex]["homepage"] !== undefined ? + //% "Event website" + ("" + qsTrId("#eventWebsite") + ""):"" selector.appear(selectOptions, control.widgetData["competitions"][compIndex]['name'], eventWebsite + ((eventWebsite !== "" && infosheet !== "") ? ", ":"") + infosheet ) } @@ -214,7 +232,8 @@ DataListView { } } - selector.appear(selectOptions, qsTr("select year")) + //% "Select year" + selector.appear(selectOptions, qsTrId("#selectYear")) } function openCup(state, data) { @@ -226,7 +245,8 @@ DataListView { if(state === undefined){ // opened for the first time -> select cup - selectTitle = qsTr("select cup") + //% "Select cup" + selectTitle = qsTrId("#selectCup") cups.sort(function(a, b) { return parseInt(b["SerId"]) - parseInt(a["SerId"]); @@ -254,7 +274,8 @@ DataListView { return } - selectTitle = cup['name'] + ": " + qsTr("select category") + //% "Select category" + selectTitle = cup['name'] + ": " + qsTrId("#selectCategory") // build a list with all cat in the cup out of the cat keys (rkey) given in the cup.cats for(prop in cup['cats']){ @@ -383,6 +404,10 @@ DataListView { app.openWidget({cup: data.cup, cat: data.cat}) } } + + function onLinkActivated(link) { + Qt.openUrlExternally(link) + } } header: Item { @@ -403,12 +428,13 @@ DataListView { SelectorPopup { id: filterSelectPu + Material.theme: control.Material.theme + contentItem: ListView { id: selectorLv property int delegateHeight: 50 spacing: 10 - clip: true implicitHeight: model === 0 ? 0:(delegateHeight + spacing) * model diff --git a/resources/qml/Widgets/ProfileWidget.qml b/resources/qml/Widgets/ProfileWidget.qml index 5f0caa7..f7a6745 100644 --- a/resources/qml/Widgets/ProfileWidget.qml +++ b/resources/qml/Widgets/ProfileWidget.qml @@ -35,6 +35,15 @@ Page { property var widgetData: currentWidgetData + property Component headerComponent: ToolButton { + id: shareToolBt + + onClicked: shareWidget(control.title) + + text: "\uf1e0" + font.family: fa5solid.name + } + Component.onCompleted: { control.ready = true control.status = 200 @@ -190,7 +199,8 @@ Page { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - text: qsTr("age") + ": " + widgetData["age"] + //% "Age" + text: qsTrId("#age") + ": " + widgetData["age"] } Label { @@ -206,7 +216,8 @@ Page { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - text: qsTr("year of birth") + ": " + widgetData["birthdate"] + //% "Year of birth" + text: qsTrId("#yearOfBirth") + ": " + widgetData["birthdate"] } Label { @@ -222,7 +233,8 @@ Page { verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - text: qsTr("city") + ": " + widgetData["city"] + //% "City" + text: qsTrId("#city") + ": " + widgetData["city"] } } @@ -251,7 +263,7 @@ Page { height: 1 width: parent.width - color: "black" + color: Material.foreground } @@ -263,7 +275,11 @@ Page { flat: true - text: bestResultsRep.showAllResults ? qsTr("show best results"):qsTr("show all results") + text: bestResultsRep.showAllResults ? + //% "Show best results" + qsTrId("#showBestResults"): + //% "Show all results" + qsTrId("#showAllResults") onClicked: { bestResultsRep.showAllResults = !bestResultsRep.showAllResults diff --git a/resources/qml/Widgets/RankingWidget.qml b/resources/qml/Widgets/RankingWidget.qml index 7a3acd8..c63a9b4 100644 --- a/resources/qml/Widgets/RankingWidget.qml +++ b/resources/qml/Widgets/RankingWidget.qml @@ -27,12 +27,22 @@ DataListView { property bool ready property string title: control.widgetData['comp_name'] - property string subTitle: qsTr("(Ranking)") + " after " + control.widgetData['route_name'] + //% "(Ranking)" + property string subTitle: qsTrId("#rankingHeadline") + " after " + control.widgetData['route_name'] property bool titleIsPageTitle: true property var widgetData: currentWidgetData signal closeAll() + property Component headerComponent: ToolButton { + id: shareToolBt + + onClicked: shareWidget(control.title) + + text: "\uf1e0" + font.family: fa5solid.name + } + Connections { target: selector function onSelectionFinished(index, data) { @@ -80,7 +90,7 @@ DataListView { } onPressAndHold: { - app.openWidget({person:thisData["PerId"]}) + app.openWidget({person:thisData["PerId"]}) } ParallelAnimation { @@ -91,6 +101,8 @@ DataListView { text: "" + highlighted: partDel.thisIndex % 2 == 0 + onClicked: { if(state === "closed"){ // close all other delegates @@ -110,16 +122,6 @@ DataListView { } } - Rectangle { - anchors.fill: parent - - width: partDel.width - - color: partDel.thisIndex % 2 == 0 ? "white":"lightgrey" - - opacity: 0.2 - } - Column { id: partDelCol diff --git a/resources/qml/Widgets/RegistrationWidget.qml b/resources/qml/Widgets/RegistrationWidget.qml index 106d27e..dd4cc75 100644 --- a/resources/qml/Widgets/RegistrationWidget.qml +++ b/resources/qml/Widgets/RegistrationWidget.qml @@ -18,6 +18,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.12 import "../Components" @@ -30,17 +31,34 @@ DataListView { property string subTitle: getSubtitle() property bool titleIsPageTitle: true - property Component headerComponent: ToolButton { - id: moreToolBt + property Component headerComponent: RowLayout { - onClicked: { - control.changeCat() + height: parent.height + spacing: 0 + + ToolButton { + id: shareToolBt + + onClicked: shareWidget(control.title) + + text: "\uf1e0" + font.family: fa5solid.name } - icon.name: "menu" + ToolButton { + id: moreToolBt + + onClicked: { + control.changeCat() + } + + text: "\uf142" + font.family: fa5solid.name + } } property var widgetData: currentWidgetData + property var athletes function getSubtitle() { var titleString @@ -55,18 +73,16 @@ DataListView { } } - var addition = "(Registration) " + //% "(Registration)" + var addition = qsTrId("#registrationHeadline") - if(titleString !== undefined){ + if(titleString) return addition + " " + titleString - } - else { + else return "" - } - } - function getText(index){ + function getText(athleteData){ var fedName // federation name @@ -75,21 +91,20 @@ DataListView { for(var i = 0; i < widgetData["federations"].length; i ++ ){ //console.log("checking " + i + ": cat: " + parseInt(widgetData["categorys"][i]["GrpId"]) + " searched cat: " + root.catId) - if(widgetData["federations"][i]["fed_id"] === widgetData[ 'athletes' ][index]["reg_fed_id"]){ + if(widgetData["federations"][i]["fed_id"] === athleteData["reg_fed_id"]){ fedName = widgetData["federations"][i]["shortcut"] } } } else { // an international competition -> get nation - - fedName = widgetData[ 'athletes' ][index]["nation"] + fedName = athleteData["nation"] } - return widgetData[ "athletes" ][index]["firstname"] + " " + widgetData[ "athletes" ][index]["lastname"] + " (" + fedName + ")" + return athleteData["firstname"] + " " + athleteData["lastname"] + " (" + fedName + ")" } - function changeCat(){ + function changeCat() { var cats = control.widgetData["categorys"] cats.sort(function(a, b) { @@ -106,7 +121,22 @@ DataListView { } } - selector.appear(selectOptions, qsTr("select cat")) + selector.appear(selectOptions, qsTrId("#selectCategory"), + //% "Show results" + "" + qsTrId("#showResults") + "") + } + + function filterAthletes(athletes) { + if(!params.cat) { + params.cat = control.widgetData["categorys"][0]["GrpId"] + } + + var filtered = athletes.filter(function(athlete){ + var res = String(athlete.cat).toLowerCase() === String(params.cat).toLowerCase() + return res + }) + + return filtered } Connections { @@ -116,14 +146,18 @@ DataListView { updateData({cat: data.cat}, true) } } + + function onLinkActivated(link) { + selector.close() + var tmpParams = params + tmpParams.type = "" + app.openWidget(tmpParams) + } } - status: model === 0 ? 901:200 - - model: widgetData[ 'athletes' ] === undefined ? 0:widgetData[ 'athletes' ].length - Component.onCompleted: { - if(model > 0){ + athletes = filterAthletes(widgetData["athletes"]) + if(athletes.length > 0){ control.ready = true control.status = 200 } @@ -137,18 +171,24 @@ DataListView { updateData({}, false) } + onWidgetDataChanged: { + athletes = [] + athletes = filterAthletes(widgetData["athletes"]) + } + + model: athletes + delegate: ItemDelegate { id: partDel property int thisIndex: index - property var thisData: widgetData[ "athletes" ][index] - - width: parent.width - height: parseInt(thisData.cat) === parseInt(params.cat) ? 50:0 + property var thisData: modelData opacity: 0 scale: 0.9 + width: control.width + onThisDataChanged: { fadeInPa.start() } @@ -165,15 +205,7 @@ DataListView { text: "" - Rectangle { - anchors.fill: parent - - width: partDel.width - - color: partDel.thisIndex % 2 == 0 ? "white":"lightgrey" - - opacity: 0.2 - } + highlighted: partDel.thisIndex % 2 == 0 Label { anchors.fill: parent @@ -186,7 +218,7 @@ DataListView { elide: "ElideRight" - text: control.getText(index) + text: control.getText(partDel.thisData) } } diff --git a/resources/qml/Widgets/ResultWidget.qml b/resources/qml/Widgets/ResultWidget.qml index b44c2f4..1ac9ef1 100644 --- a/resources/qml/Widgets/ResultWidget.qml +++ b/resources/qml/Widgets/ResultWidget.qml @@ -46,7 +46,8 @@ DataListView { onClicked: control.changeCat() - icon.name: "menu" + text: "\uf142" + font.family: fa5solid.name } ToolButton { @@ -61,7 +62,17 @@ DataListView { speedFlowChartPopup.toggle() } - icon.name: "flowchart" + text: "\uf0e8" + font.family: fa5solid.name + } + + ToolButton { + id: shareToolBt + + onClicked: shareWidget(control.title) + + text: "\uf1e0" + font.family: fa5solid.name } } @@ -113,25 +124,15 @@ DataListView { } function getSubtitle() { - var titleString + var titleString = control.widgetData["route_name"] - for(var i = 0; i < control.widgetData["categorys"].length; i ++ ){ - //console.log("checking " + i + ": cat: " + parseInt(control.widgetData["categorys"][i]["GrpId"]) + " searched cat: " + params.cat) - if(parseInt(control.widgetData["categorys"][i]["GrpId"]) === parseInt(params.cat)){ - titleString = control.widgetData["categorys"][i]["name"] - } - } + //% "(Results)" + var addition = qsTrId("#resultsHeadline") - var addition = qsTr("(Results)") - - if(titleString !== undefined){ + if(titleString) return addition + " " + titleString - } - else { + else return "" - } - - } function changeRoute(route) { @@ -155,7 +156,7 @@ DataListView { } } - selector.appear(selectOptions, qsTr("select cat")) + selector.appear(selectOptions, qsTrId("#selectCategory")) } Connections { diff --git a/resources/qml/Widgets/StartlistWidget.qml b/resources/qml/Widgets/StartlistWidget.qml index 90959d1..4de9d02 100644 --- a/resources/qml/Widgets/StartlistWidget.qml +++ b/resources/qml/Widgets/StartlistWidget.qml @@ -19,6 +19,7 @@ import QtQuick 2.9 import QtQuick.Controls 2.4 import QtGraphicalEffects 1.0 +import QtQuick.Layouts 1.12 import "../Components" @@ -28,32 +29,45 @@ DataListView { property bool ready property string title: control.widgetData['comp_name'] - property string subTitle: qsTr("(Startlist)") + " " + control.widgetData['route_name'] //getSubtitle() + property string subTitle: getSubtitle() property bool titleIsPageTitle: true - property Component headerComponent: ToolButton { + property Component headerComponent: RowLayout { + + height: parent.height + spacing: 0 + + ToolButton { + id: shareToolBt + + onClicked: shareWidget(control.title) + + text: "\uf1e0" + font.family: fa5solid.name + } + + ToolButton { id: moreToolBt onClicked: { control.changeCat() } - icon.name: "menu" + text: "\uf142" + font.family: fa5solid.name + } } function getSubtitle() { - var titleString + var titleString = control.widgetData["route_name"] - for(var i = 0; i < control.widgetData["categorys"].length; i ++ ){ - //console.log("checking " + i + ": cat: " + parseInt(control.widgetData["categorys"][i]["GrpId"]) + " searched cat: " + params.cat) - if(parseInt(control.widgetData["categorys"][i]["GrpId"]) === parseInt(params.cat)){ - titleString = control.widgetData["categorys"][i]["name"] - } - } - - var addition = qsTr("(Startlist)") - return addition + " " + titleString + //% "(Startlist)" + var addition = qsTrId("#startlistHeadline") + if(titleString) + return addition + " " + titleString + else + return "" } function changeRoute(route) { @@ -77,7 +91,7 @@ DataListView { } } - selector.appear(selectOptions, qsTr("select cat")) + selector.appear(selectOptions, qsTrId("#selectCategory")) } Connections { @@ -116,7 +130,7 @@ DataListView { property int thisIndex: index property var thisData: widgetData[ "participants" ][index] - width: parent.width + width: control.width height: 50 opacity: 0 @@ -138,15 +152,7 @@ DataListView { text: "" - Rectangle { - anchors.fill: parent - - width: partDel.width - - color: partDel.thisIndex % 2 == 0 ? "white":"lightgrey" - - opacity: 0.2 - } + highlighted: partDel.thisIndex % 2 == 0 Row { id: partDelFirstRow diff --git a/resources/qml/main.qml b/resources/qml/main.qml index fb5c8df..72c8c6c 100644 --- a/resources/qml/main.qml +++ b/resources/qml/main.qml @@ -23,34 +23,102 @@ import QtQuick.Layouts 1.3 import QtQuick.Controls.Material 2.12 import QtPurchasing 1.12 -import com.itsblue.digitalRockRanking 1.0 +import de.itsblue.blueROCK 1.0 import "./Pages" import "./Components" +import "./Widgets" + Window { visible: true width: 540 height: 960 - title: qsTr("blueROCK") + title: "blueROCK" Page { id: app property int errorCode: -1 + property var colorShade: Material.theme === Material.Light ? Material.Shade300 : Material.Shade700 + property color nationalAdultsColor: Material.color(Material.Green, colorShade) + property color nationalYouthColor: Material.color(Material.LightGreen, colorShade) + property color federalColor: Material.color(Material.Grey, colorShade) + // comp cats source: // - https://github.com/ralfbecker/ranking/blob/master/sitemgr/digitalrock/dav_calendar.php // - https://github.com/ralfbecker/ranking/blob/master/sitemgr/digitalrock/sac_calendar.php property var compCats: { + // --- ICC --- + + /*'int' : { + 'label' : 'International', + 'nation' : 'ICC', + 'wettk_reg' : '^[0-9]{2,2}[_^E]{1}[^YJ]{1,1}.*', + 'serie_reg' : '^[0-9]{2,2}_(WC|TR){1,1}.*', + 'rang_title': 'CUWR continuously updated WORLDRANKING', + 'bgcolor' : '#B8C8FF', + 'nat_team_ranking' : '', + 'cat_id' : [68,86]//[68,69,70,86,259] + },*/ + 'worldcup': { + 'label' : 'World Cups', + 'nation' : 'ICC', + 'bgcolor' : '#B8C8FF', + 'sort_rank': 1, + 'cat_id' : [69] + }, + 'youth' : { + 'label' : 'Youth Events', + 'nation' : 'ICC', + 'wettk_reg' : '^[0-9]{2,2}(EYC|_J|_Y){1,1}.*', + 'serie_reg' : '^[0-9]{2,2}_EYC', + 'rang_title': '', + 'bgcolor' : '#D8E8FF', + 'sort_rank': 2, + 'cat_id' : [71,258] + }, + 'cont': { + 'label' : 'Continental Events', + 'nation' : 'ICC', + 'bgcolor' : '#B8C8FF', + 'sort_rank': 3, + 'cat_id' : [262] + }, + 'masters' : { + 'label' : 'Masters and Promo Events', + 'nation' : 'ICC', + 'wettk_reg' : '^[0-9]{2,2}_[^PWERASL]{1}.*', + // 'serie_reg' : '^[0-9]{2,2}_(WC|TR){1,1}.*', + // 'rang_title': 'CUWR continuously updated WORLDRANKING', + 'bgcolor' : '#F0F0F0', + 'sort_rank': 4, + 'cat_id' : [70] + }, + 'para' : { + 'label' : 'Paraclimbing Events', + 'nation' : 'ICC', + 'wettk_reg' : '^[0-9]{2,2}_PE.*', + 'bgcolor' : '#F0F0F0', + 'sort_rank': 5, + 'cat_id' : [256,259] + }, + 'games': { + 'label': 'Games', + 'nation': 'ICC', + 'bgcolor' : '#B8C8FF', + 'sort_rank': 6, + 'cat_id': [68,86] + }, // --- GER --- 'ger_meisterschaft' : { 'label' : 'Deutsche Meisterschaft', 'nation' : 'GER', - 'bgcolor' : '#A8F0A8', + 'bgcolor' : app.nationalAdultsColor,//'#A8F0A8', 'sort_rank': 1, 'cat_id' : [57, 59, 60] }, @@ -61,7 +129,7 @@ Window { 'wettk_reg' : '^[0-9]{2,2}[_J]{1,1}[^WL]+.*', 'serie_reg' : '^[0-9]{2,2}_JC', // 'rang_title': 'Deutsche Jugend RANGLISTE', - 'bgcolor' : '#D8FFD8', + 'bgcolor' : app.nationalYouthColor,//'#D8FFD8', 'sort_rank': 2, 'cat_id' : [58] }, @@ -71,7 +139,7 @@ Window { 'wettk_reg' : '^[0-9]{2,2}[_J]{1,1}LM.*', 'serie_reg' : '^[0-9]{2,2}[_J]{1,1}LM.*', 'rang_title': '', - 'bgcolor' : '#F0F0F0', + 'bgcolor' : app.federalColor, //'#F0F0F0', 'sort_rank': 3, 'cat_id' : [61,56] }, @@ -84,7 +152,7 @@ Window { 'wettk_reg' : '^[0-9]{2,2}_[^R].*', 'serie_reg' : '.*', 'rang_title': 'SWISS RANKING', - 'bgcolor' : '#A8F0A8', + 'bgcolor' : app.nationalAdultsColor, //'#A8F0A8', 'sort_rank': 1, 'cat_id' : [62,63] }, @@ -94,7 +162,7 @@ Window { 'wettk_reg' : '^[0-9]{2,2}_[^R].*', 'serie_reg' : '.*', 'rang_title': 'SWISS RANKING', - 'bgcolor' : '#D8FFD8', + 'bgcolor' : app.nationalYouthColor, //'#D8FFD8', 'sort_rank': 2, 'cat_id' : [65] }, @@ -103,16 +171,16 @@ Window { 'nation' : 'SUI', 'wettk_reg' : '^[0-9]{2,2}_RG_.*', 'rang_title': '', - 'bgcolor' : '#F0F0F0', + 'bgcolor' : app.federalColor, //'#F0F0F0', 'sort_rank': 3, 'cat_id' : [64] }, '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' : '#F0F0F0', + 'bgcolor' : app.federalColor, //'#F0F0F0', 'sort_rank': 4, 'cat_id' : [84] } @@ -120,7 +188,7 @@ Window { anchors.fill: parent - //Material.theme: Material.Dark + Material.theme: appSettings.read("darkTheme") === "true" ? Material.Dark:Material.Light Component.onCompleted: { //loadingDl.open() @@ -129,6 +197,18 @@ Window { //mainStack.push("Pages/AthleteSearchPage.qml") //openWidget({comp: 11651, cat: 26}) //openWidget({person: 6623}) + //console.log(JSON.stringify(serverConn.getParamsFromUrl(""))) + //openWidgetFromUrl("https://l.bluerock.dev/?comp=11501&cat=GER_M") + } + + FontLoader { + id: fa5solid + source: "qrc:/fonts/fa5solid.otf" + } + + FontLoader { + id: fa5regular + source: "qrc:/fonts/fa5regular.otf" } Shortcut { @@ -137,8 +217,12 @@ Window { onActivated: app.goBack() } - ServerConn { + BlueRockBackend { id: serverConn + + onOpenedViaUrl: { + app.openWidgetFromUrl(url) + } } AppSettings { @@ -225,7 +309,9 @@ Window { height: parent.height onClicked: app.goBack() - icon.name: "back" + + text: "\uf053" + font.family: fa5solid.name } Column { @@ -235,20 +321,16 @@ Window { height: childrenRect.height width: parent.width - extraComponentLoader.width - toolButton.width - 3 * parent.spacing - Label { + MovingLabel { id: toolBarTitleLa + syncWithLabel: toolBarSubTitleLa width: parent.width - scale: 1 - elide: "ElideRight" - font.bold: true verticalAlignment: Text.AlignVCenter - color: "black" - text: getText() function getText(){ @@ -277,18 +359,16 @@ Window { } } - Label { + MovingLabel { id: toolBarSubTitleLa - width: parent.width - height: text !== "" ? undefined:0 + visible: text !== "" - elide: "ElideRight" + syncWithLabel: toolBarTitleLa + width: parent.width font.bold: false - color: "black" - text: getText() function getText(){ @@ -298,7 +378,7 @@ Window { titleString = mainStack.currentItem.subTitle } - return(titleString) + return titleString } Behavior on text { @@ -466,7 +546,8 @@ Window { font.bold: true color: "white" - text: "loading..." + //% "Loading" + text: qsTrId("#loading") + "..." } } @@ -507,6 +588,13 @@ Window { return app.height < app.width } + function toggleDarkMode() { + var dark = app.Material.theme === Material.Light + + app.Material.theme = dark ? Material.Dark : Material.Light + appSettings.write("darkTheme", dark) + } + function largeScreen() { return Math.min(app.width, app.height) > 750 } @@ -514,17 +602,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) { @@ -549,66 +656,36 @@ Window { // 2 - error var errorString - var errorDescription switch(errorCode) { case 0: infoLevel = 2 - errorString = "No connection to server" - errorDescription = "Please check your internet connection and try again." + //% "No connection to server" + errorString = qsTrId("#noConnectionError") break - case 200: - infoLevel = 0 - errorString = "Success" - errorDescription = "The request was successfull" - break - case 401: + case 404: infoLevel = 2 - errorString = "Authentication required" - errorDescription = "The server asked for user credentinals, please chack them and try again" - break - case 500: - infoLevel = 2 - errorString = "Internal server error" - errorDescription = "The server was unable to process this request, this is probaply the servers fault. Please try again later." - break - case 900: - infoLevel = 2 - errorString = "Internal error" - errorDescription = "Something went wron internally, this is probaply an inssue in the program code" + //% "Not found" + errorString = qsTrId("#notFoundError") break case 901: infoLevel = 1 - errorString = "No Data" - errorDescription = "There is currently no data available. Please try again later." + //% "No Data" + errorString = qsTrId("#noDataError") break - case 902: - infoLevel = 1 - errorString = "Cached (old) data" - errorDescription = "Es konnte keine Verbindung zum Server hergestellt werden, aber es sind noch alte Daten gespeichert." - break - case 903: - infoLevel = 1 - errorString = "Ungültiger Aufruf" - errorDescription = "Die aufgerufene Funktion ist momentan nicht verfügbar, bitte versuche es später erneut." - break - case 904: + case 906: infoLevel = 2 - errorString = "Incompatible API" - errorDescription = "Please make shure that you are using the latest version of this app and try again." - break - case 905: - infoLevel = 1 - errorString = "Loading..." - errorDescription = "Please wait while we're loading some data" + //% "Invalid Request" + errorString = qsTrId("#invalidRequestError") + errorDescription = "Invalid Request" break default: infoLevel = 2 - errorString = "Unexpected error ("+errorCode+")" - errorDescription = "Unexpected error while getting data from the server. Please try again later." + //% "Unexpected error" + errorString = qsTrId("#unexpectedError") + " ("+errorCode+")" } - return([infoLevel, errorString, errorDescription]) + return([infoLevel, errorString]) } } diff --git a/resources/qml/qml.qrc b/resources/qml/qml.qrc index 81bf996..323af13 100644 --- a/resources/qml/qml.qrc +++ b/resources/qml/qml.qrc @@ -26,5 +26,10 @@ Components/SpeedFlowChartPopup.qml Components/BlueRockBadge.qml Components/DisclaimerDialog.qml + Components/ColoredItemDelegate.qml + Components/AlignedButton.qml + Components/SharePopup.qml + Pages/QrCodeScanPage.qml + Components/MovingLabel.qml diff --git a/resources/shared/PosterTemplate.png b/resources/shared/PosterTemplate.png new file mode 100644 index 0000000..1548bb5 Binary files /dev/null and b/resources/shared/PosterTemplate.png differ diff --git a/resources/shared/PosterTemplate.xcf b/resources/shared/PosterTemplate.xcf new file mode 100644 index 0000000..152385b Binary files /dev/null and b/resources/shared/PosterTemplate.xcf differ diff --git a/resources/shared/fonts/OpenSans-Light.ttf b/resources/shared/fonts/OpenSans-Light.ttf new file mode 100644 index 0000000..6580d3a Binary files /dev/null and b/resources/shared/fonts/OpenSans-Light.ttf differ diff --git a/resources/shared/fonts/fa5regular.otf b/resources/shared/fonts/fa5regular.otf new file mode 100644 index 0000000..cc0ba27 Binary files /dev/null and b/resources/shared/fonts/fa5regular.otf differ diff --git a/resources/shared/fonts/fa5solid.otf b/resources/shared/fonts/fa5solid.otf new file mode 100644 index 0000000..5a83ab9 Binary files /dev/null and b/resources/shared/fonts/fa5solid.otf differ diff --git a/resources/shared/icons/bluerock/20x20/back.png b/resources/shared/icons/bluerock/20x20/back.png deleted file mode 100644 index db43e27..0000000 Binary files a/resources/shared/icons/bluerock/20x20/back.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/bookmark.png b/resources/shared/icons/bluerock/20x20/bookmark.png deleted file mode 100644 index c01e7bf..0000000 Binary files a/resources/shared/icons/bluerock/20x20/bookmark.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/bookmarkFilled.png b/resources/shared/icons/bluerock/20x20/bookmarkFilled.png deleted file mode 100644 index db854f9..0000000 Binary files a/resources/shared/icons/bluerock/20x20/bookmarkFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/buy.png b/resources/shared/icons/bluerock/20x20/buy.png deleted file mode 100644 index 9f9c09f..0000000 Binary files a/resources/shared/icons/bluerock/20x20/buy.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/calendar.png b/resources/shared/icons/bluerock/20x20/calendar.png deleted file mode 100644 index 61cc427..0000000 Binary files a/resources/shared/icons/bluerock/20x20/calendar.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/cup.png b/resources/shared/icons/bluerock/20x20/cup.png deleted file mode 100644 index d52dbd1..0000000 Binary files a/resources/shared/icons/bluerock/20x20/cup.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/drawer.png b/resources/shared/icons/bluerock/20x20/drawer.png deleted file mode 100644 index 1e974ef..0000000 Binary files a/resources/shared/icons/bluerock/20x20/drawer.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/filter.png b/resources/shared/icons/bluerock/20x20/filter.png deleted file mode 100644 index bcc1e8a..0000000 Binary files a/resources/shared/icons/bluerock/20x20/filter.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/flowchart.png b/resources/shared/icons/bluerock/20x20/flowchart.png deleted file mode 100644 index 6469b0a..0000000 Binary files a/resources/shared/icons/bluerock/20x20/flowchart.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/menu.png b/resources/shared/icons/bluerock/20x20/menu.png deleted file mode 100644 index a10473d..0000000 Binary files a/resources/shared/icons/bluerock/20x20/menu.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/pin.png b/resources/shared/icons/bluerock/20x20/pin.png deleted file mode 100644 index 9908baf..0000000 Binary files a/resources/shared/icons/bluerock/20x20/pin.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/pinFilled.png b/resources/shared/icons/bluerock/20x20/pinFilled.png deleted file mode 100644 index fe79afb..0000000 Binary files a/resources/shared/icons/bluerock/20x20/pinFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/star.png b/resources/shared/icons/bluerock/20x20/star.png deleted file mode 100644 index 55d76f3..0000000 Binary files a/resources/shared/icons/bluerock/20x20/star.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/starFilled.png b/resources/shared/icons/bluerock/20x20/starFilled.png deleted file mode 100644 index 78aec7c..0000000 Binary files a/resources/shared/icons/bluerock/20x20/starFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20/year.png b/resources/shared/icons/bluerock/20x20/year.png deleted file mode 100644 index 26f4edc..0000000 Binary files a/resources/shared/icons/bluerock/20x20/year.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/back.png b/resources/shared/icons/bluerock/20x20@2/back.png deleted file mode 100644 index c55ab31..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/back.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/bookmark.png b/resources/shared/icons/bluerock/20x20@2/bookmark.png deleted file mode 100644 index 47d18cb..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/bookmark.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/bookmarkFilled.png b/resources/shared/icons/bluerock/20x20@2/bookmarkFilled.png deleted file mode 100644 index 1dcf9d4..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/bookmarkFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/buy.png b/resources/shared/icons/bluerock/20x20@2/buy.png deleted file mode 100644 index 2ae229d..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/buy.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/calendar.png b/resources/shared/icons/bluerock/20x20@2/calendar.png deleted file mode 100644 index 53fe9b1..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/calendar.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/cup.png b/resources/shared/icons/bluerock/20x20@2/cup.png deleted file mode 100644 index b66aa2f..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/cup.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/drawer.png b/resources/shared/icons/bluerock/20x20@2/drawer.png deleted file mode 100644 index eba3b6c..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/drawer.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/filter.png b/resources/shared/icons/bluerock/20x20@2/filter.png deleted file mode 100644 index 1f88edc..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/filter.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/flowchart.png b/resources/shared/icons/bluerock/20x20@2/flowchart.png deleted file mode 100644 index c87cc2e..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/flowchart.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/menu.png b/resources/shared/icons/bluerock/20x20@2/menu.png deleted file mode 100644 index 649c2a0..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/menu.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/pin.png b/resources/shared/icons/bluerock/20x20@2/pin.png deleted file mode 100644 index 0af60ed..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/pin.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/pinFilled.png b/resources/shared/icons/bluerock/20x20@2/pinFilled.png deleted file mode 100644 index 8c3b904..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/pinFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/star.png b/resources/shared/icons/bluerock/20x20@2/star.png deleted file mode 100644 index 4104d59..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/star.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/starFilled.png b/resources/shared/icons/bluerock/20x20@2/starFilled.png deleted file mode 100644 index 4ff5390..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/starFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@2/year.png b/resources/shared/icons/bluerock/20x20@2/year.png deleted file mode 100644 index 980d2ee..0000000 Binary files a/resources/shared/icons/bluerock/20x20@2/year.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/back.png b/resources/shared/icons/bluerock/20x20@3/back.png deleted file mode 100644 index b228eb8..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/back.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/bookmark.png b/resources/shared/icons/bluerock/20x20@3/bookmark.png deleted file mode 100644 index ca812b1..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/bookmark.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/bookmarkFilled.png b/resources/shared/icons/bluerock/20x20@3/bookmarkFilled.png deleted file mode 100644 index 0dfcd81..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/bookmarkFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/buy.png b/resources/shared/icons/bluerock/20x20@3/buy.png deleted file mode 100644 index a34fe40..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/buy.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/calendar.png b/resources/shared/icons/bluerock/20x20@3/calendar.png deleted file mode 100644 index 434503c..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/calendar.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/cup.png b/resources/shared/icons/bluerock/20x20@3/cup.png deleted file mode 100644 index 9ad32ba..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/cup.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/drawer.png b/resources/shared/icons/bluerock/20x20@3/drawer.png deleted file mode 100644 index 3584ed6..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/drawer.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/filter.png b/resources/shared/icons/bluerock/20x20@3/filter.png deleted file mode 100644 index b457f02..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/filter.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/flowchart.png b/resources/shared/icons/bluerock/20x20@3/flowchart.png deleted file mode 100644 index 98bd45f..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/flowchart.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/menu.png b/resources/shared/icons/bluerock/20x20@3/menu.png deleted file mode 100644 index 9554b69..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/menu.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/pin.png b/resources/shared/icons/bluerock/20x20@3/pin.png deleted file mode 100644 index 306add5..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/pin.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/pinFilled.png b/resources/shared/icons/bluerock/20x20@3/pinFilled.png deleted file mode 100644 index c7eac9c..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/pinFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/star.png b/resources/shared/icons/bluerock/20x20@3/star.png deleted file mode 100644 index 0b03be1..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/star.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/starFilled.png b/resources/shared/icons/bluerock/20x20@3/starFilled.png deleted file mode 100644 index 3984352..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/starFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@3/year.png b/resources/shared/icons/bluerock/20x20@3/year.png deleted file mode 100644 index 5c2eb8f..0000000 Binary files a/resources/shared/icons/bluerock/20x20@3/year.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/back.png b/resources/shared/icons/bluerock/20x20@4/back.png deleted file mode 100644 index dd157e7..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/back.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/bookmark.png b/resources/shared/icons/bluerock/20x20@4/bookmark.png deleted file mode 100644 index c82e389..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/bookmark.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/bookmarkFilled.png b/resources/shared/icons/bluerock/20x20@4/bookmarkFilled.png deleted file mode 100644 index a021439..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/bookmarkFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/buy.png b/resources/shared/icons/bluerock/20x20@4/buy.png deleted file mode 100644 index ef6c793..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/buy.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/calendar.png b/resources/shared/icons/bluerock/20x20@4/calendar.png deleted file mode 100644 index d799f8c..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/calendar.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/cup.png b/resources/shared/icons/bluerock/20x20@4/cup.png deleted file mode 100644 index 4baaf9a..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/cup.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/drawer.png b/resources/shared/icons/bluerock/20x20@4/drawer.png deleted file mode 100644 index 60d93af..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/drawer.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/filter.png b/resources/shared/icons/bluerock/20x20@4/filter.png deleted file mode 100644 index 891fcae..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/filter.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/flowchart.png b/resources/shared/icons/bluerock/20x20@4/flowchart.png deleted file mode 100644 index dc859de..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/flowchart.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/menu.png b/resources/shared/icons/bluerock/20x20@4/menu.png deleted file mode 100644 index 187c171..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/menu.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/pin.png b/resources/shared/icons/bluerock/20x20@4/pin.png deleted file mode 100644 index e193f91..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/pin.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/pinFilled.png b/resources/shared/icons/bluerock/20x20@4/pinFilled.png deleted file mode 100644 index f92d9a9..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/pinFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/star.png b/resources/shared/icons/bluerock/20x20@4/star.png deleted file mode 100644 index cd38f03..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/star.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/starFilled.png b/resources/shared/icons/bluerock/20x20@4/starFilled.png deleted file mode 100644 index 7895700..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/starFilled.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/20x20@4/year.png b/resources/shared/icons/bluerock/20x20@4/year.png deleted file mode 100644 index bc9ab79..0000000 Binary files a/resources/shared/icons/bluerock/20x20@4/year.png and /dev/null differ diff --git a/resources/shared/icons/bluerock/index.theme b/resources/shared/icons/bluerock/index.theme deleted file mode 100644 index 2457fde..0000000 --- a/resources/shared/icons/bluerock/index.theme +++ /dev/null @@ -1,24 +0,0 @@ -[Icon Theme] -Name=blueROCK -Comment=blueROCK Icon Theme - -Directories=20x20,20x20@2,20x20@3,20x20@4 - -[20x20] -Size=20 -Type=Fixed - -[20x20@2] -Size=20 -Scale=2 -Type=Fixed - -[20x20@3] -Size=20 -Scale=3 -Type=Fixed - -[20x20@4] -Size=20 -Scale=4 -Type=Fixed diff --git a/resources/shared/icons/dav-dark.png b/resources/shared/icons/dav-dark.png new file mode 100644 index 0000000..ff29824 Binary files /dev/null and b/resources/shared/icons/dav-dark.png differ diff --git a/resources/shared/icons/dig_rock.klein.jpg b/resources/shared/icons/dig_rock.klein.jpg deleted file mode 100644 index 7170811..0000000 Binary files a/resources/shared/icons/dig_rock.klein.jpg and /dev/null differ diff --git a/resources/shared/icons/dig_rock.klein.png b/resources/shared/icons/dig_rock.klein.png deleted file mode 100644 index 0eb0963..0000000 Binary files a/resources/shared/icons/dig_rock.klein.png and /dev/null differ diff --git a/resources/shared/icons/ifsc.png b/resources/shared/icons/ifsc.png deleted file mode 100644 index 993f23a..0000000 Binary files a/resources/shared/icons/ifsc.png and /dev/null differ diff --git a/resources/shared/icons/sac-dark.png b/resources/shared/icons/sac-dark.png new file mode 100644 index 0000000..eae3e00 Binary files /dev/null and b/resources/shared/icons/sac-dark.png differ diff --git a/resources/shared/shared.qrc b/resources/shared/shared.qrc index 5c58276..5475fd3 100644 --- a/resources/shared/shared.qrc +++ b/resources/shared/shared.qrc @@ -1,7 +1,6 @@ icons/dav.png - icons/ifsc.png icons/sac.png icons/back.png icons/backDark.png @@ -9,70 +8,7 @@ icons/cup.png Banner.png icons/more_black.png - icons/bluerock/20x20/back.png - icons/bluerock/20x20/cup.png - icons/bluerock/20x20/drawer.png - icons/bluerock/20x20/menu.png - icons/bluerock/20x20@2/back.png - icons/bluerock/20x20@2/cup.png - icons/bluerock/20x20@2/drawer.png - icons/bluerock/20x20@2/menu.png - icons/bluerock/20x20@3/back.png - icons/bluerock/20x20@3/cup.png - icons/bluerock/20x20@3/drawer.png - icons/bluerock/20x20@3/menu.png - icons/bluerock/20x20@4/back.png - icons/bluerock/20x20@4/cup.png - icons/bluerock/20x20@4/drawer.png - icons/bluerock/20x20@4/menu.png - icons/bluerock/index.theme - icons/dig_rock.klein.jpg - icons/dig_rock.klein.png - icons/bluerock/20x20/calendar.png - icons/bluerock/20x20@2/calendar.png - icons/bluerock/20x20@3/calendar.png - icons/bluerock/20x20@4/calendar.png - icons/bluerock/20x20/filter.png - icons/bluerock/20x20/year.png - icons/bluerock/20x20@2/filter.png - icons/bluerock/20x20@2/year.png - icons/bluerock/20x20@3/filter.png - icons/bluerock/20x20@3/year.png - icons/bluerock/20x20@4/filter.png - icons/bluerock/20x20@4/year.png - icons/bluerock/20x20/flowchart.png - icons/bluerock/20x20@2/flowchart.png - icons/bluerock/20x20@3/flowchart.png - icons/bluerock/20x20@4/flowchart.png - icons/bluerock/20x20/bookmark.png - icons/bluerock/20x20/bookmarkFilled.png - icons/bluerock/20x20@2/bookmark.png - icons/bluerock/20x20@2/bookmarkFilled.png - icons/bluerock/20x20@3/bookmark.png - icons/bluerock/20x20@3/bookmarkFilled.png - icons/bluerock/20x20@4/bookmark.png - icons/bluerock/20x20@4/bookmarkFilled.png - icons/bluerock/20x20/buy.png - icons/bluerock/20x20@2/buy.png - icons/bluerock/20x20@3/buy.png - icons/bluerock/20x20@4/buy.png icons/lock.png - icons/bluerock/20x20/star.png - icons/bluerock/20x20/starFilled.png - icons/bluerock/20x20@2/star.png - icons/bluerock/20x20@2/starFilled.png - icons/bluerock/20x20@3/star.png - icons/bluerock/20x20@3/starFilled.png - icons/bluerock/20x20@4/star.png - icons/bluerock/20x20@4/starFilled.png - icons/bluerock/20x20/pin.png - icons/bluerock/20x20/pinFilled.png - icons/bluerock/20x20@2/pin.png - icons/bluerock/20x20@2/pinFilled.png - icons/bluerock/20x20@3/pin.png - icons/bluerock/20x20@3/pinFilled.png - icons/bluerock/20x20@4/pin.png - icons/bluerock/20x20@4/pinFilled.png icons/blueRockHold.png screenshots/SpeedFlowchartDemo/android/1.jpeg screenshots/SpeedFlowchartDemo/android/2.jpeg @@ -80,5 +16,11 @@ screenshots/SpeedFlowchartDemo/ios/1.jpeg screenshots/SpeedFlowchartDemo/ios/2.jpeg screenshots/SpeedFlowchartDemo/ios/3.jpeg + icons/dav-dark.png + icons/sac-dark.png + fonts/fa5regular.otf + fonts/fa5solid.otf + PosterTemplate.png + fonts/OpenSans-Light.ttf diff --git a/resources/translations/de.qm b/resources/translations/de.qm new file mode 100644 index 0000000..919c85c Binary files /dev/null and b/resources/translations/de.qm differ diff --git a/resources/translations/de.ts b/resources/translations/de.ts new file mode 100644 index 0000000..5b1713c --- /dev/null +++ b/resources/translations/de.ts @@ -0,0 +1,276 @@ + + + + + + + + + Scan QR-Code + QR-Code scannen + + + + Place the Code in the center + Positioniere den Code in der Mitte + + + + Camera access required + Camera access denied + Kamerazugriff benötigt + + + + blueROCK needs to access your camera in order to scan QR-Codes. It will never record or store any photos or videos. + This app needs to access your camera in order to scan QR-Codes. It will never record or store any photos or videos. + blueROCK benötig Zugriff auf die Kamera um QR-Codes zu scannen. Es werden keine Fotos oder Videos aufgezeichnet oder gespeichert. + + + + Allow access + Zugriff zulassen + + + + Plase wait + Bitte warten + + + + Invalid QR-Code + Ungültiger QR-Code + + + + Share these results + Teile diese Ergebnisse + + + + Link + Link + + + + QR-Code + QR-Code + + + + Poster + Poster + + + + Purchase failed + Kauf fehlgeschlagen + + + + + Buy now for + Jetzt kaufen für + + + This item is currently unavailable + Diese Produkt ist nicht verfügbar + + + + This is a premium feature. + Das ist eine premium Funktion. + + + + + This item is currently unavailable + Dieses Produkt ist nicht verfügbar + + + + Restore purchase + Kauf wiederherstellen + + + + contact support + Support kontaktieren + + + + IFSC results + IFSC Ergebnisse + + + + Dark mode + Dunkler Modus + + + + Light mode + Heller Modus + + + + About blueROCK + Über blueROCK + + + + Where are the IFSC results? + Wo sind die IFSC Ergebnisse? + + + + 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>. + 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>. + + + + privacy policy + Datenschutzerklärung + + + + 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/blueROCK/app'>here</a>.<br><br>Resultservice and rankings provided by <a href='http://www.digitalROCK.de'>digital ROCK</a>. + 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>. + 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/blueROCK/app'>hier</a>.<br><br>Die Ergebnisse und Ranglisten werden von <a href='http://www.digitalROCK.de'>digital ROCK</a> zur Verfügung gestellt. + + + + calendar + Kalender + + + + Favorites + Favoriten + + + + Select filters + Filter auswählen + + + + Infosheet + Ausschreibung + + + + Further infos + Weitere Informationen + + + + Event website + Veranstaltungswebseite + + + + Select year + Jahr auswählen + + + + Select cup + Rangliste auswählen + + + + + + + Select category + Kategorie auswählen + + + + Age + Alter + + + + Year of birth + Geburtsjahr + + + + City + Stadt + + + + Show best results + Zeige die besten Ergebnisse + + + + Show all results + Zeige alle Ergebnisse + + + + (Ranking) + (Rangliste) + + + + (Registration) + (Registrierung) + + + + Show results + Ergebnisse anzeigen + + + + (Results) + (Ergebnisse) + + + + (Startlist) + (Startliste) + + + + Loading + Laden + + + + No connection to server + Keine Verbindung zum Server + + + + Not found + Nicht gefunden + + + + No Data + Keine Daten + + + + Invalid Request + Ungültige Anfrage + + + + Unexpected error + Unerwarteter Fehler + + + + Check out the results of %1 over here: + Check out the results of %1 over here: + Verfolge die Ergebnisse von "%1" hier: + + + diff --git a/resources/translations/en.qm b/resources/translations/en.qm new file mode 100644 index 0000000..6633c42 Binary files /dev/null and b/resources/translations/en.qm differ diff --git a/resources/translations/en.ts b/resources/translations/en.ts new file mode 100644 index 0000000..ac3c954 --- /dev/null +++ b/resources/translations/en.ts @@ -0,0 +1,272 @@ + + + + + + + + + Scan QR-Code + + + + + Place the Code in the center + + + + + Camera access required + Camera access denied + + + + + blueROCK needs to access your camera in order to scan QR-Codes. It will never record or store any photos or videos. + This app needs to access your camera in order to scan QR-Codes. It will never record or store any photos or videos. + + + + + Allow access + + + + + Plase wait + + + + + Invalid QR-Code + + + + + Share these results + + + + + Link + + + + + QR-Code + + + + + Poster + + + + + Purchase failed + + + + + + Buy now for + + + + + This is a premium feature. + + + + + + This item is currently unavailable + + + + + Restore purchase + + + + + contact support + + + + + IFSC results + + + + + Dark mode + + + + + Light mode + + + + + About blueROCK + + + + + Where are the IFSC results? + + + + + 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>. + + + + + privacy policy + + + + + 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/blueROCK/app'>here</a>.<br><br>Resultservice and rankings provided by <a href='http://www.digitalROCK.de'>digital ROCK</a>. + 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>. + + + + + calendar + + + + + Favorites + + + + + Select filters + + + + + Infosheet + + + + + Further infos + + + + + Event website + + + + + Select year + + + + + Select cup + + + + + + + + Select category + + + + + Age + + + + + Year of birth + + + + + City + + + + + Show best results + + + + + Show all results + + + + + (Ranking) + + + + + (Registration) + + + + + Show results + + + + + (Results) + + + + + (Startlist) + + + + + Loading + + + + + No connection to server + + + + + Not found + + + + + No Data + + + + + Invalid Request + + + + + Unexpected error + + + + + Check out the results of %1 over here: + Check out the results of %1 over here: + + + + diff --git a/resources/translations/translations.qrc b/resources/translations/translations.qrc new file mode 100644 index 0000000..a028120 --- /dev/null +++ b/resources/translations/translations.qrc @@ -0,0 +1,6 @@ + + + de.qm + en.qm + + diff --git a/sources/appsettings.cpp b/sources/appsettings.cpp index 9d98c94..b0f377c 100644 --- a/sources/appsettings.cpp +++ b/sources/appsettings.cpp @@ -15,8 +15,6 @@ AppSettings::AppSettings(QObject* parent) // set the values to their defaults if they haven't been created yet - // create or open the settings.ini file - this->themeSettingsManager = new QSettings(":/themes/" + this->read("theme") + ".ini", QSettings::IniFormat); } QString AppSettings::read(const QString &key) @@ -26,7 +24,7 @@ QString AppSettings::read(const QString &key) // open the value-group this->settingsManager->beginGroup("AppSettings"); // read the value - QString value = this->settingsManager->value(key , false).toString(); + QString value = this->settingsManager->value(key, false).toString(); // close the value-group this->settingsManager->endGroup(); // return the value @@ -51,7 +49,7 @@ void AppSettings::setDefault(const QString &key, const QVariant &defaultValue) // read the current value QString value = this->read(key); - if(value == "false"){ + if(value == "false") { // if it is nor defined yet, the read function will return "false" (as a string) // -> if that is the case -> create the key with the default value this->write(key, defaultValue); diff --git a/sources/bluerockbackend.cpp b/sources/bluerockbackend.cpp new file mode 100644 index 0000000..8abc6c9 --- /dev/null +++ b/sources/bluerockbackend.cpp @@ -0,0 +1,323 @@ +/* + blueROCK - for digital rock + Copyright (C) 2019 Dorian Zedler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "headers/bluerockbackend.h" + +BlueRockBackend::BlueRockBackend(QObject *parent) : QObject(parent), _pendingIntentsChecked(false) +{ + this->_shareUtils = new ShareUtils(this); + connect(this->_shareUtils, &ShareUtils::otherUrlReceived, this, &BlueRockBackend::openedViaUrl); + +#if defined(Q_OS_ANDROID) + connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(onApplicationStateChanged(Qt::ApplicationState))); +#elif defined Q_OS_IOS + this->_iosPermissionUtils = new IosPermissionUtils(); +#endif +} + +QVariant BlueRockBackend::getWidgetData(QVariantMap params) { + QString requestUrl = "https://www.digitalrock.de/egroupware/ranking/json.php?"; + QStringList nations = {"ICC", "GER", "SUI"}; + if(params["nation"].toString() == "ICC") { + params["nation"] = ""; + } + else if(params["nation"].toString() == "") { + params.remove("nation"); + } + else if(!nations.contains(params["nation"].toString())) { + // a non-empty nation which ist not one of the above is invalid + return QVariantMap({{"status", 404}}); + } + + for(QVariantMap::const_iterator iter = params.begin(); iter != params.end(); ++iter) { + requestUrl += iter.key() + "=" + iter.value().toString() + "&"; + } + + requestUrl = requestUrl.left(requestUrl.length() - 1); // remove last '&' + + qWarning() << requestUrl; + + QVariantMap ret = this->_senddata(QUrl(requestUrl)); + + if(ret["status"] != 200) { + // request was a failure + return QVariantMap({{"status", ret["status"]}, {"data", ""}}); + } + + QJsonDocument jsonReply = QJsonDocument::fromJson(ret["text"].toString().toUtf8()); + + QVariantMap data = {{"status", 200}, {"data", jsonReply.toVariant()}}; + + return data; +} + + +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; + + for(QPair pair : query.queryItems()) { + if(params.contains(pair.first)) + params[pair.first] = pair.second; + else + params.insert(pair.first, pair.second); + } + + return {{"valid", true},{"params",params}}; +} + +void BlueRockBackend::shareResultsAsUrl(QString url, QString compName) { + //% "Check out the results of %1 over here:" + this->_shareUtils->shareText(qtTrId("#shareResultsLinkText").arg(compName) + " \n", url); +} + +void BlueRockBackend::shareResultsAsPoster(QString url, QString compName) { + QString path = this->_shareUtils->getTemporaryFileLocationPath(); //QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + if (!QDir(path).exists()) { + if (QDir("").mkpath(path)) { + qDebug() << "Created app data /files directory. " << path; + } else { + qWarning() << "Failed to create app data /files directory. " << path; + return; + } + } + + QString rawCompName = compName; + QString escapedCompName = compName.replace("/", " "); + path += "/" + escapedCompName + ".pdf"; + + QFile file(path); + file.remove(); + if(!file.open(QIODevice::ReadWrite)) { + qWarning("Could not open File for writing!!"); + return; + } + + // dimensions + QRect qrCodeRect = QRect(414, 414, 1650, 1650); + + int compNameTextLineHeight = 64; + QFont compNameTextFont("OpenSans-Light"); + compNameTextFont.setPixelSize(compNameTextLineHeight); + QRect compNameTextRect = QRect( + 324, + 2500, + 1835, + 150 + ); + + QPdfWriter writer(&file); + writer.setPageSize(QPageSize(QPageSize::A4)); + writer.setPageMargins(QMargins(0, 0, 0, 0)); + writer.setResolution(300); + + QPainter painter(&writer); + QPixmap posterTemplate(":/PosterTemplate.png"); + painter.drawPixmap(0,0, writer.width(), writer.height(), posterTemplate); + + QPixmap barcode; + QZXingEncoderConfig encoderConfig(QZXing::EncoderFormat_QR_CODE, qrCodeRect.size(), QZXing::EncodeErrorCorrectionLevel_H, false, false); + barcode.convertFromImage(QZXing::encodeData(url, encoderConfig)); + painter.drawPixmap(qrCodeRect, barcode); + + painter.setFont(compNameTextFont); + painter.setPen(Qt::black); + painter.drawText(compNameTextRect, Qt::AlignLeft|Qt::AlignBottom|Qt::TextWordWrap, rawCompName); + + painter.end(); + file.close(); + + this->_shareUtils->sendFile(path, escapedCompName, "application/pdf", 1); +} + +bool BlueRockBackend::isCameraPermissionGranted() { +#ifdef Q_OS_ANDROID + QtAndroid::PermissionResult cameraAccess = QtAndroid::checkPermission("android.permission.CAMERA"); + return cameraAccess == QtAndroid::PermissionResult::Granted; +#elif defined Q_OS_IOS + return this->_iosPermissionUtils->isCameraPermissionGranted(); +#else + return true; +#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("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()); + 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()); + 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()); + intent.callObjectMethod("setData", "(Landroid/net/Uri;)Landroid/content/Intent;", uri.object()); + + activity.callMethod("startActivity","(Landroid/content/Intent;)V",intent.object()); + } + else { + qWarning() << "ERROR: Activity not valid!"; + return false; + } + + return false; +#elif defined Q_OS_IOS + return this->_iosPermissionUtils->requestCameraPermission(); +#else + return true; +#endif + +} + +// ------------------------ +// --- Helper functions --- +// ------------------------ + +QVariantMap BlueRockBackend::_senddata(QUrl serviceUrl, QUrlQuery pdata) +{ + // create network manager + QNetworkAccessManager * networkManager = new QNetworkAccessManager(); + + QVariantMap ret; //this is a custom type to store the return-data + + // Create network request + QNetworkRequest request(serviceUrl); + request.setHeader(QNetworkRequest::ContentTypeHeader, + "application/x-www-form-urlencoded"); + + //QSslConfiguration config = QSslConfiguration::defaultConfiguration(); + //config.setProtocol(QSsl::TlsV1_2); + //request.setSslConfiguration(config); + + //send a POST request with the given url and data to the server + + QNetworkReply *reply; + + if(pdata.isEmpty()) { + // if no post data is given -> send a GET request + reply = networkManager->get(request); + } + else { + // if post data is given -> send POST request + reply = networkManager->post(request, pdata.toString(QUrl::FullyEncoded).toUtf8()); + } + + // loop to wait until the request has finished before processing the data + QEventLoop loop; + // timer to cancel the request after 3 seconds + QTimer timer; + timer.setSingleShot(true); + + // quit the loop when the request finised + loop.connect(networkManager, SIGNAL(finished(QNetworkReply*)), SLOT(quit())); + // or the timer timed out + loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); + // start the timer + timer.start(10000); + // start the loop + loop.exec(); + + //get the status code + QVariant status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); + + ret.insert("status", status_code.toInt()); + + //get the full text response + ret.insert("text", QString::fromUtf8(reply->readAll())); + + // delete the reply object + delete reply; + + // delete the newtwork access manager object + delete networkManager; + + //return the data + return(ret); +} + +#if defined(Q_OS_ANDROID) +void BlueRockBackend::onApplicationStateChanged(Qt::ApplicationState applicationState) +{ + qDebug() << "S T A T E changed into: " << applicationState; + if(applicationState == Qt::ApplicationState::ApplicationSuspended) { + // nothing to do + return; + } + if(applicationState == Qt::ApplicationState::ApplicationActive) { + // if App was launched from VIEW or SEND Intent + // there's a race collision: the event will be lost, + // because App and UI wasn't completely initialized + // workaround: QShareActivity remembers that an Intent is pending + if(!_pendingIntentsChecked) { + _pendingIntentsChecked = true; + _shareUtils->checkPendingIntents(this->_shareUtils->getTemporaryFileLocationPath()); + } + } +} +#endif + +// ------------------------- +// --- Functions for QML --- +// ------------------------- diff --git a/sources/iospermissionutils.mm b/sources/iospermissionutils.mm new file mode 100644 index 0000000..7935404 --- /dev/null +++ b/sources/iospermissionutils.mm @@ -0,0 +1,34 @@ +#import "../headers/iospermissionutils.h" +#import +#import + +IosPermissionUtils::IosPermissionUtils() : QObject(nullptr) +{ + this->_responseWaitLoop = new QEventLoop(this); +} + +bool IosPermissionUtils::isCameraPermissionGranted() { + AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + return status == AVAuthorizationStatusAuthorized; +} + +bool IosPermissionUtils::requestCameraPermission() { + if(this->isCameraPermissionGranted()) + return true; + + if ([AVCaptureDevice respondsToSelector:@selector(requestAccessForMediaType: completionHandler:)]) { + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { + // Will get here on both iOS 7 & 8 even though camera permissions weren't required + // until iOS 8. So for iOS 7 permission will always be granted. + this->_responseWaitLoop->exit(granted); + }]; + } + + if(!this->_responseWaitLoop->exec()) { + // permission was not granted -> open settings + [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; + return false; + } + + return true; +} diff --git a/sources/main.cpp b/sources/main.cpp index e099c5c..fb66458 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -20,10 +20,11 @@ #include #include #include -#include -#include +#include +#include +#include "QZXing.h" -#include "headers/serverconn.h" +#include "headers/bluerockbackend.h" #include "headers/appsettings.h" int main(int argc, char *argv[]) @@ -33,12 +34,22 @@ int main(int argc, char *argv[]) QGuiApplication app(argc, argv); - QQuickStyle::setStyle("Material"); - QIcon::setFallbackSearchPaths(QIcon::fallbackSearchPaths() << ":/resources/shared/icons"); - QIcon::setThemeName("bluerock"); + // translation + QString localeName = QLocale::system().bcp47Name(); + QTranslator* translator = new QTranslator(qApp); - qmlRegisterType("com.itsblue.digitalRockRanking", 1, 0, "ServerConn"); - qmlRegisterType("com.itsblue.digitalRockRanking", 1, 0, "AppSettings"); + // fallback to en! + if(!translator->load(":/" + localeName + ".qm")) + translator->load(":/en.qm"); + + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Light.ttf"); + + QGuiApplication::installTranslator(translator); + + QQuickStyle::setStyle("Material"); + + qmlRegisterType("de.itsblue.blueROCK", 1, 0, "BlueRockBackend"); + qmlRegisterType("de.itsblue.blueROCK", 1, 0, "AppSettings"); QQmlApplicationEngine engine; @@ -48,6 +59,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"))); diff --git a/sources/serverconn.cpp b/sources/serverconn.cpp deleted file mode 100644 index 2279dec..0000000 --- a/sources/serverconn.cpp +++ /dev/null @@ -1,135 +0,0 @@ -/* - blueROCK - for digital rock - Copyright (C) 2019 Dorian Zedler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "headers/serverconn.h" - -ServerConn::ServerConn(QObject *parent) : QObject(parent) -{ -} - -QVariant ServerConn::getWidgetData(QVariantMap params) { - QString requestUrl; - if(params["nation"].toString() == "ICC") { - requestUrl = "https://www.digitalrock.de/egroupware/ranking/json.php?"; - params["nation"] = ""; - } - else if (params["nation"].toString() == "GER") { - requestUrl = "https://www.digitalrock.de/egroupware/ranking/json.php?"; - } - else if (params["nation"].toString() == "SUI") { - requestUrl = "https://www.digitalrock.de/egroupware/ranking/json.php?"; - } - else { - params.remove("nation"); - requestUrl = "https://www.digitalrock.de/egroupware/ranking/json.php?"; - } - - for(QVariantMap::const_iterator iter = params.begin(); iter != params.end(); ++iter) { - requestUrl += iter.key() + "=" + iter.value().toString() + "&"; - } - - requestUrl = requestUrl.left(requestUrl.length() - 1); // remove last '&' - - qDebug() << requestUrl; - - QVariantMap ret = this->senddata(QUrl(requestUrl)); - - if(ret["status"] != 200) { - // request was a failure - return QVariantMap({{"status", ret["status"]}, {"data", ""}}); - } - - QJsonDocument jsonReply = QJsonDocument::fromJson(ret["text"].toString().toUtf8()); - - QVariantMap data = {{"status", 200}, {"data", jsonReply.toVariant()}}; - - return data; -} - -// ------------------------ -// --- Helper functions --- -// ------------------------ - -QVariantMap ServerConn::senddata(QUrl serviceUrl, QUrlQuery pdata) -{ - // create network manager - QNetworkAccessManager * networkManager = new QNetworkAccessManager(); - - QVariantMap ret; //this is a custom type to store the return-data - - // Create network request - QNetworkRequest request(serviceUrl); - request.setHeader(QNetworkRequest::ContentTypeHeader, - "application/x-www-form-urlencoded"); - - //QSslConfiguration config = QSslConfiguration::defaultConfiguration(); - //config.setProtocol(QSsl::TlsV1_2); - //request.setSslConfiguration(config); - - //send a POST request with the given url and data to the server - - QNetworkReply *reply; - - if(pdata.isEmpty()) { - // if no post data is given -> send a GET request - reply = networkManager->get(request); - } - else { - // if post data is given -> send POST request - reply = networkManager->post(request, pdata.toString(QUrl::FullyEncoded).toUtf8()); - } - - // loop to wait until the request has finished before processing the data - QEventLoop loop; - // timer to cancel the request after 3 seconds - QTimer timer; - timer.setSingleShot(true); - - // quit the loop when the request finised - loop.connect(networkManager, SIGNAL(finished(QNetworkReply*)), SLOT(quit())); - // or the timer timed out - loop.connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit())); - // start the timer - timer.start(10000); - // start the loop - loop.exec(); - - //get the status code - QVariant status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); - - ret.insert("status", status_code.toInt()); - - //get the full text response - ret.insert("text", QString::fromUtf8(reply->readAll())); - - // delete the reply object - delete reply; - - // delete the newtwork access manager object - delete networkManager; - - //return the data - return(ret); -} - -// ------------------------- -// --- Functions for QML --- -// ------------------------- - - - diff --git a/sources/shareUtils/androidshareutils.cpp b/sources/shareUtils/androidshareutils.cpp new file mode 100755 index 0000000..f3fdf64 --- /dev/null +++ b/sources/shareUtils/androidshareutils.cpp @@ -0,0 +1,379 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#include "shareUtils/androidshareutils.h" + +#include +#include +#include + +#include +#include + +const static int RESULT_OK = -1; +const static int RESULT_CANCELED = 0; + +AndroidShareUtils* AndroidShareUtils::mInstance = NULL; + +AndroidShareUtils::AndroidShareUtils(QObject* parent) : PlatformShareUtils(parent) +{ + // we need the instance for JNI Call + mInstance = this; +} + +AndroidShareUtils* AndroidShareUtils::getInstance() +{ + if (!mInstance) { + mInstance = new AndroidShareUtils; + qWarning() << "AndroidShareUtils should be instantiated !"; + } + + return mInstance; +} + +bool AndroidShareUtils::checkMimeTypeView(const QString &mimeType) +{ + QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType); + jboolean verified = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "checkMimeTypeView", + "(Ljava/lang/String;)Z", + jsMime.object()); + qDebug() << "View VERIFIED: " << mimeType << " - " << verified; + return verified; +} + +bool AndroidShareUtils::checkMimeTypeEdit(const QString &mimeType) +{ + QAndroidJniObject jsMime = QAndroidJniObject::fromString(mimeType); + jboolean verified = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "checkMimeTypeEdit", + "(Ljava/lang/String;)Z", + jsMime.object()); + qDebug() << "Edit VERIFIED: " << mimeType << " - " << verified; + return verified; +} + +QString AndroidShareUtils::getTemporaryFileLocationPath() { + return QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0) + "/temporaryFiles"; +} + +void AndroidShareUtils::shareText(const QString &text, const QUrl &url) +{ + QAndroidJniObject jsText = QAndroidJniObject::fromString(text + url.toString()); + jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "shareText", + "(Ljava/lang/String;)Z", + jsText.object()); + + if(!ok) { + qWarning() << "Unable to resolve activity from Java"; + emit shareNoAppAvailable(0); + } +} + +/* + * As default we're going the Java - way with one simple JNI call (recommended) + * if altImpl is true we're going the pure JNI way + * + * If a requestId was set we want to get the Activity Result back (recommended) + * We need the Request Id and Result Id to control our workflow +*/ +void AndroidShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mIsEditMode = false; + + QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); + QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); + QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); + jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "sendFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); + if(!ok) { + qWarning() << "Unable to resolve activity from Java"; + emit shareNoAppAvailable(requestId); + } +} + +/* + * As default we're going the Java - way with one simple JNI call (recommended) + * if altImpl is true we're going the pure JNI way + * + * If a requestId was set we want to get the Activity Result back (recommended) + * We need the Request Id and Result Id to control our workflow +*/ +void AndroidShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mIsEditMode = false; + + QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); + QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); + QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); + jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "viewFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); + if(!ok) { + qWarning() << "Unable to resolve activity from Java"; + emit shareNoAppAvailable(requestId); + } +} + +/* + * As default we're going the Java - way with one simple JNI call (recommended) + * if altImpl is true we're going the pure JNI way + * + * If a requestId was set we want to get the Activity Result back (recommended) + * We need the Request Id and Result Id to control our workflow +*/ +void AndroidShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mIsEditMode = true; + mCurrentFilePath = filePath; + QFileInfo fileInfo = QFileInfo(mCurrentFilePath); + mLastModified = fileInfo.lastModified().toSecsSinceEpoch(); + qDebug() << "LAST MODIFIED: " << mLastModified; + + QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath); + QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title); + QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType); + + jboolean ok = QAndroidJniObject::callStaticMethod("org/ekkescorner/utils/QShareUtils", + "editFile", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z", + jsPath.object(), jsTitle.object(), jsMimeType.object(), requestId); + + if(!ok) { + qWarning() << "Unable to resolve activity from Java"; + emit shareNoAppAvailable(requestId); + } +} + +// used from QAndroidActivityResultReceiver +void AndroidShareUtils::handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data) +{ + Q_UNUSED(data); + qDebug() << "From JNI QAndroidActivityResultReceiver: " << receiverRequestCode << "ResultCode:" << resultCode; + processActivityResult(receiverRequestCode, resultCode); +} + +// used from Activity.java onActivityResult() +void AndroidShareUtils::onActivityResult(int requestCode, int resultCode) +{ + qDebug() << "From Java Activity onActivityResult: " << requestCode << "ResultCode:" << resultCode; + processActivityResult(requestCode, resultCode); +} + +void AndroidShareUtils::processActivityResult(int requestCode, int resultCode) +{ + // we're getting RESULT_OK only if edit is done + if(resultCode == RESULT_OK) { + emit shareEditDone(requestCode); + } else if(resultCode == RESULT_CANCELED) { + if(mIsEditMode) { + // Attention: not all Apps will give you the correct ResultCode: + // Google Fotos will send OK if saved and CANCELED if canceled + // Some Apps always sends CANCELED even if you modified and Saved the File + // so you should check the modified Timestamp of the File to know if + // you should emit shareEditDone() or shareFinished() !!! + QFileInfo fileInfo = QFileInfo(mCurrentFilePath); + qint64 currentModified = fileInfo.lastModified().toSecsSinceEpoch(); + qDebug() << "CURRENT MODIFIED: " << currentModified; + if(currentModified > mLastModified) { + emit shareEditDone(requestCode); + return; + } + } + emit shareFinished(requestCode); + } else { + qDebug() << "wrong result code: " << resultCode << " from request: " << requestCode; + emit shareError(requestCode, "Share: an Error occured"); + } +} + +void AndroidShareUtils::checkPendingIntents(const QString workingDirPath) +{ + QAndroidJniObject activity = QtAndroid::androidActivity(); + if(activity.isValid()) { + // create a Java String for the Working Dir Path + QAndroidJniObject jniWorkingDir = QAndroidJniObject::fromString(workingDirPath); + if(!jniWorkingDir.isValid()) { + qWarning() << "QAndroidJniObject jniWorkingDir not valid."; + emit shareError(0, "Share: an Error occured\nWorkingDir not valid"); + return; + } + activity.callMethod("checkPendingIntents","(Ljava/lang/String;)V", jniWorkingDir.object()); + qDebug() << "checkPendingIntents: " << workingDirPath; + return; + } + qDebug() << "checkPendingIntents: Activity not valid"; +} + +void AndroidShareUtils::setFileUrlReceived(const QString &url) +{ + if(url.isEmpty()) { + qWarning() << "setFileUrlReceived: we got an empty URL"; + emit shareError(0, "Empty URL received"); + return; + } + qDebug() << "AndroidShareUtils setFileUrlReceived: we got the File URL from JAVA: " << url; + QString myUrl; + if(url.startsWith("file://")) { + myUrl= url.right(url.length()-7); + qDebug() << "QFile needs this URL: " << myUrl; + } else { + myUrl= url; + } + + // check if File exists + QFileInfo fileInfo = QFileInfo(myUrl); + if(fileInfo.exists()) { + emit fileUrlReceived(myUrl); + } else { + qDebug() << "setFileUrlReceived: FILE does NOT exist "; + emit shareError(0, QString("File does not exist: %1").arg(myUrl)); + } +} + +void AndroidShareUtils::setOtherUrlReceived(const QString &url, const QString &scheme) +{ + if(url.isEmpty()) { + qWarning() << "setFileUrlReceived: we got an empty URL"; + emit shareError(0, "Empty URL received"); + return; + } + qDebug() << "AndroidShareUtils setOtherUrlReceived: we got the Other URL from JAVA: " << url; + + emit otherUrlReceived(url, scheme); +} + +void AndroidShareUtils::setFileReceivedAndSaved(const QString &url) +{ + if(url.isEmpty()) { + qWarning() << "setFileReceivedAndSaved: we got an empty URL"; + emit shareError(0, "Empty URL received"); + return; + } + qDebug() << "AndroidShareUtils setFileReceivedAndSaved: we got the File URL from JAVA: " << url; + QString myUrl; + if(url.startsWith("file://")) { + myUrl= url.right(url.length()-7); + qDebug() << "QFile needs this URL: " << myUrl; + } else { + myUrl= url; + } + + // check if File exists + QFileInfo fileInfo = QFileInfo(myUrl); + if(fileInfo.exists()) { + emit fileReceivedAndSaved(myUrl); + } else { + qDebug() << "setFileReceivedAndSaved: FILE does NOT exist "; + emit shareError(0, QString("File does not exist: %1").arg(myUrl)); + } +} + +// to be safe we check if a File Url from java really exists for Qt +// if not on the Java side we'll try to read the content as Stream +bool AndroidShareUtils::checkFileExits(const QString &url) +{ + if(url.isEmpty()) { + qWarning() << "checkFileExits: we got an empty URL"; + emit shareError(0, "Empty URL received"); + return false; + } + qDebug() << "AndroidShareUtils checkFileExits: we got the File URL from JAVA: " << url; + QString myUrl; + if(url.startsWith("file://")) { + myUrl= url.right(url.length()-7); + qDebug() << "QFile needs this URL: " << myUrl; + } else { + myUrl= url; + } + + // check if File exists + QFileInfo fileInfo = QFileInfo(myUrl); + if(fileInfo.exists()) { + qDebug() << "Yep: the File exists for Qt"; + return true; + } else { + qDebug() << "Uuups: FILE does NOT exist "; + return false; + } +} + +// instead of defining all JNICALL as demonstrated below +// there's another way, making it easier to manage all the methods +// see https://www.kdab.com/qt-android-episode-5/ + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL +Java_de_itsblue_blueROCK_MainActivity_setFileUrlReceived(JNIEnv *env, + jobject obj, + jstring url) +{ + const char *urlStr = env->GetStringUTFChars(url, NULL); + Q_UNUSED (obj) + AndroidShareUtils::getInstance()->setFileUrlReceived(urlStr); + env->ReleaseStringUTFChars(url, urlStr); + return; +} + +JNIEXPORT void JNICALL +Java_de_itsblue_blueROCK_MainActivity_setOtherUrlReceived(JNIEnv *env, + jobject obj, + jstring url, + jstring scheme) +{ + const char *urlStr = env->GetStringUTFChars(url, NULL); + const char *schemeStr = env->GetStringUTFChars(scheme, NULL); + Q_UNUSED (obj) + AndroidShareUtils::getInstance()->setOtherUrlReceived(urlStr, schemeStr); + env->ReleaseStringUTFChars(url, urlStr); + env->ReleaseStringUTFChars(scheme, schemeStr); + return; +} + +JNIEXPORT void JNICALL +Java_de_itsblue_blueROCK_MainActivity_setFileReceivedAndSaved(JNIEnv *env, + jobject obj, + jstring url) +{ + const char *urlStr = env->GetStringUTFChars(url, NULL); + Q_UNUSED (obj) + AndroidShareUtils::getInstance()->setFileReceivedAndSaved(urlStr); + env->ReleaseStringUTFChars(url, urlStr); + return; +} + +JNIEXPORT bool JNICALL +Java_de_itsblue_blueROCK_MainActivity_checkFileExits(JNIEnv *env, + jobject obj, + jstring url) +{ + const char *urlStr = env->GetStringUTFChars(url, NULL); + Q_UNUSED (obj) + bool exists = AndroidShareUtils::getInstance()->checkFileExits(urlStr); + env->ReleaseStringUTFChars(url, urlStr); + return exists; +} + +JNIEXPORT void JNICALL +Java_de_itsblue_blueROCK_MainActivity_fireActivityResult(JNIEnv *env, + jobject obj, + jint requestCode, + jint resultCode) +{ + Q_UNUSED (obj) + Q_UNUSED (env) + AndroidShareUtils::getInstance()->onActivityResult(requestCode, resultCode); + return; +} + +#ifdef __cplusplus +} +#endif diff --git a/sources/shareUtils/ios/docviewcontroller.mm b/sources/shareUtils/ios/docviewcontroller.mm new file mode 100644 index 0000000..c4a5e36 --- /dev/null +++ b/sources/shareUtils/ios/docviewcontroller.mm @@ -0,0 +1,32 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#import "headers/shareUtils/ios/docviewcontroller.h" + +#include + +@interface DocViewController () +@end +@implementation DocViewController +#pragma mark - +#pragma mark View Life Cycle +- (void)viewDidLoad { + [super viewDidLoad]; +} +#pragma mark - +#pragma mark Document Interaction Controller Delegate Methods +- (UIViewController *) documentInteractionControllerViewControllerForPreview: (UIDocumentInteractionController *) controller { +#pragma unused (controller) + return self; +} +- (void)documentInteractionControllerDidEndPreview:(UIDocumentInteractionController *)controller +{ +#pragma unused (controller) + qDebug() << "end preview"; + + self.mIosShareUtils->handleDocumentPreviewDone(self.requestId); + + [self removeFromParentViewController]; +} +@end diff --git a/sources/shareUtils/ios/iosshareutils.mm b/sources/shareUtils/ios/iosshareutils.mm new file mode 100755 index 0000000..a8bc5b6 --- /dev/null +++ b/sources/shareUtils/ios/iosshareutils.mm @@ -0,0 +1,156 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#import "headers/shareUtils/ios/iosshareutils.h" + +#import +#import +#import +#import +#import +#import + +#import + +#import "headers/shareUtils/ios/docviewcontroller.h" + +IosShareUtils::IosShareUtils(QObject *parent) : PlatformShareUtils(parent) +{ + // Sharing Files from other iOS Apps I got the ideas and some code contribution from: + // Thomas K. Fischer (@taskfabric) - http://taskfabric.com - thx + QDesktopServices::setUrlHandler("file", this, "handleFileUrlReceived"); + QDesktopServices::setUrlHandler("https", this, "handleHttpsUrlReceived"); +} + +bool IosShareUtils::checkMimeTypeView(const QString &mimeType) { +#pragma unused (mimeType) + // dummi implementation on iOS + // MimeType not used yet + return true; +} + +bool IosShareUtils::checkMimeTypeEdit(const QString &mimeType) { +#pragma unused (mimeType) + // dummi implementation on iOS + // MimeType not used yet + return true; +} + +void IosShareUtils::shareText(const QString &text, const QUrl &url) { + + NSMutableArray *sharingItems = [NSMutableArray new]; + + if (!text.isEmpty()) { + [sharingItems addObject:text.toNSString()]; + } + + if (url.isValid()) { + [sharingItems addObject:url.toNSURL()]; + } + + // get the main window rootViewController + UIViewController *qtUIViewController = [[UIApplication sharedApplication].keyWindow rootViewController]; + + UIActivityViewController *activityController = [[UIActivityViewController alloc] initWithActivityItems:sharingItems applicationActivities:nil]; + if ( [activityController respondsToSelector:@selector(popoverPresentationController)] ) { // iOS8 + activityController.popoverPresentationController.sourceView = qtUIViewController.view; + } + [qtUIViewController presentViewController:activityController animated:YES completion:nil]; +} + +// altImpl not used yet on iOS, on Android twi ways to use JNI +void IosShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { +#pragma unused (title, mimeType) + + NSString* nsFilePath = filePath.toNSString(); + NSURL *nsFileUrl = [NSURL fileURLWithPath:nsFilePath]; + + static DocViewController* docViewController = nil; + if(docViewController!=nil) + { + [docViewController removeFromParentViewController]; + [docViewController release]; + } + + UIDocumentInteractionController* documentInteractionController = nil; + documentInteractionController = [UIDocumentInteractionController interactionControllerWithURL:nsFileUrl]; + + UIViewController* qtUIViewController = [[[[UIApplication sharedApplication]windows] firstObject]rootViewController]; + if(qtUIViewController!=nil) + { + docViewController = [[DocViewController alloc] init]; + + docViewController.requestId = requestId; + // we need this to be able to execute handleDocumentPreviewDone() method, + // when preview was finished + docViewController.mIosShareUtils = this; + + [qtUIViewController addChildViewController:docViewController]; + documentInteractionController.delegate = docViewController; + // [documentInteractionController presentPreviewAnimated:YES]; + if(![documentInteractionController presentPreviewAnimated:YES]) + { + emit shareError(0, tr("No App found to open: %1").arg(filePath)); + } + } +} + + +void IosShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { +#pragma unused (title, mimeType) + + sendFile(filePath, title, mimeType, requestId); +} + +void IosShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { +#pragma unused (title, mimeType) + + sendFile(filePath, title, mimeType, requestId); +} + +void IosShareUtils::handleDocumentPreviewDone(const int &requestId) +{ + // documentInteractionControllerDidEndPreview + qDebug() << "handleShareDone: " << requestId; + emit shareFinished(requestId); +} + +void IosShareUtils::handleFileUrlReceived(const QUrl &url) +{ + QString incomingUrl = url.toString(); + if(incomingUrl.isEmpty()) { + qWarning() << "setFileUrlReceived: we got an empty URL"; + emit shareError(0, tr("Empty URL received")); + return; + } + qDebug() << "IosShareUtils setFileUrlReceived: we got the File URL from iOS: " << incomingUrl; + QString myUrl; + if(incomingUrl.startsWith("file://")) { + myUrl= incomingUrl.right(incomingUrl.length()-7); + qDebug() << "QFile needs this URL: " << myUrl; + } else { + myUrl= incomingUrl; + } + + // check if File exists + QFileInfo fileInfo = QFileInfo(myUrl); + if(fileInfo.exists()) { + emit fileUrlReceived(myUrl); + } else { + qDebug() << "setFileUrlReceived: FILE does NOT exist "; + emit shareError(0, tr("File does not exist: %1").arg(myUrl)); + } +} + +void IosShareUtils::handleHttpsUrlReceived(const QUrl &url) +{ + if(url.isEmpty()) { + qWarning() << "handleHttpsUrlReceived: we got an empty URL"; + emit shareError(0, tr("Empty URL received")); + return; + } + qDebug() << "IosShareUtils handleHttpsUrlReceived: we got the Other URL from IOS: " << url; + + emit otherUrlReceived(url.toString(), "https"); +} diff --git a/sources/shareUtils/platformshareutils.cpp b/sources/shareUtils/platformshareutils.cpp new file mode 100644 index 0000000..8ccc3cf --- /dev/null +++ b/sources/shareUtils/platformshareutils.cpp @@ -0,0 +1,41 @@ +#include "shareUtils/platformshareutils.h" + +PlatformShareUtils::PlatformShareUtils(QObject *parent) : QObject(parent) +{ + +} + +PlatformShareUtils::~PlatformShareUtils() { + +} + +bool PlatformShareUtils::checkMimeTypeView(const QString &mimeType) { + qDebug() << "check view for " << mimeType; + return true; +} +bool PlatformShareUtils::checkMimeTypeEdit(const QString &mimeType) { + qDebug() << "check edit for " << mimeType; + return true; +} +QString PlatformShareUtils::getTemporaryFileLocationPath() { + return QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); +} +void PlatformShareUtils::shareText(const QString &text, const QUrl &url) { + qDebug() << text << url; +} +void PlatformShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { + qDebug() << filePath << " - " << title << "requestId " << requestId << " - " << mimeType << "altImpl? "; + QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); +} +void PlatformShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { + qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? "; + QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); +} +void PlatformShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) { + qDebug() << filePath << " - " << title << " requestId: " << requestId << " - " << mimeType << "altImpl? "; + QDesktopServices::openUrl(QUrl::fromLocalFile(filePath)); +} + +void PlatformShareUtils::checkPendingIntents(const QString workingDirPath) { + qDebug() << "checkPendingIntents " << workingDirPath; +} diff --git a/sources/shareUtils/shareutils.cpp b/sources/shareUtils/shareutils.cpp new file mode 100755 index 0000000..3b4a9ec --- /dev/null +++ b/sources/shareUtils/shareutils.cpp @@ -0,0 +1,124 @@ +// (c) 2017 Ekkehard Gentz (ekke) @ekkescorner +// my blog about Qt for mobile: http://j.mp/qt-x +// see also /COPYRIGHT and /LICENSE + +#include "shareUtils/shareutils.h" + +#ifdef Q_OS_IOS +#include "shareUtils/ios/iosshareutils.h" +#endif + +#ifdef Q_OS_ANDROID +#include "shareUtils/androidshareutils.h" +#endif + +ShareUtils::ShareUtils(QObject *parent) + : QObject(parent) +{ +#if defined(Q_OS_IOS) + mPlatformShareUtils = new IosShareUtils(this); +#elif defined(Q_OS_ANDROID) + mPlatformShareUtils = new AndroidShareUtils(this); +#else + mPlatformShareUtils = new PlatformShareUtils(this); +#endif + + bool connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareEditDone, this, &ShareUtils::onShareEditDone); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareFinished, this, &ShareUtils::onShareFinished); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareNoAppAvailable, this, &ShareUtils::onShareNoAppAvailable); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::shareError, this, &ShareUtils::onShareError); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileUrlReceived, this, &ShareUtils::onFileUrlReceived); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::otherUrlReceived, this, &ShareUtils::onOtherUrlReceived); + Q_ASSERT(connectResult); + + connectResult = connect(mPlatformShareUtils, &PlatformShareUtils::fileReceivedAndSaved, this, &ShareUtils::onFileReceivedAndSaved); + Q_ASSERT(connectResult); + + Q_UNUSED(connectResult); +} + +bool ShareUtils::checkMimeTypeView(const QString &mimeType) +{ + return mPlatformShareUtils->checkMimeTypeView(mimeType); +} + +bool ShareUtils::checkMimeTypeEdit(const QString &mimeType) +{ + return mPlatformShareUtils->checkMimeTypeEdit(mimeType); +} + +QString ShareUtils::getTemporaryFileLocationPath() +{ + return mPlatformShareUtils->getTemporaryFileLocationPath(); +} + +void ShareUtils::shareText(const QString &text, const QUrl &url) +{ + mPlatformShareUtils->shareText(text, url); +} + +void ShareUtils::sendFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mPlatformShareUtils->sendFile(filePath, title, mimeType, requestId); +} + +void ShareUtils::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mPlatformShareUtils->viewFile(filePath, title, mimeType, requestId); +} + +void ShareUtils::editFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId) +{ + mPlatformShareUtils->editFile(filePath, title, mimeType, requestId); +} + +void ShareUtils::checkPendingIntents(const QString workingDirPath) +{ + mPlatformShareUtils->checkPendingIntents(workingDirPath); +} + +void ShareUtils::onShareEditDone(int requestCode) +{ + emit shareEditDone(requestCode); +} + +void ShareUtils::onShareFinished(int requestCode) +{ + emit shareFinished(requestCode); +} + +void ShareUtils::onShareNoAppAvailable(int requestCode) +{ + emit shareNoAppAvailable(requestCode); +} + +void ShareUtils::onShareError(int requestCode, QString message) +{ + emit shareError(requestCode, message); +} + +void ShareUtils::onFileUrlReceived(QString url) +{ + emit fileUrlReceived(url); +} + +void ShareUtils::onOtherUrlReceived(QString url, QString scheme) +{ + emit otherUrlReceived(url, scheme); +} + +void ShareUtils::onFileReceivedAndSaved(QString url) +{ + emit fileReceivedAndSaved(url); +} +