- release candidate 1.1.0

- added the ability to download pdf files
- added warning message "Processing error"
- new pull-to-refresh indicator
- fixed eye-ion on login screen
This commit is contained in:
dorian 2019-11-06 22:02:48 +01:00
parent 23aca9da07
commit 086e10b2f9
77 changed files with 1661 additions and 165 deletions

View File

@ -4,6 +4,18 @@ 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).
## [1.1.0] - unreleased
### Hinzugefügt
- PDF-Taste oben links in der Vertretungsplan-Ansicht, um den Vertretungsplan als PDF anzusehen
- Fehlermeldung "Verarbeitungsfehler" mit Taste zum Ansehen der PDF Datei
### Entfernt
- ständige Abfragen im Hintergrund, die sehr viel Datenvolumen verbrauchten
### Geändert
- einige Icons
- pull-to-refresh indikator
## [1.0.2] - 2019-03-14
### Hinzugefügt
- Dunkler Modus

View File

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<manifest package="com.itsblue.flgvertretungtest" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0.2" android:versionCode="16" android:installLocation="auto">
<manifest package="com.itsblue.flgvertretungbeta" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.1.0" android:versionCode="17" android:installLocation="auto">
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28"/>
<!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application.
@ -13,7 +13,7 @@
<supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/>
<application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="fannyapp" android:icon="@drawable/icon">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.qtproject.qt5.android.bindings.QtActivity" android:label="fannyapp" android:screenOrientation="unspecified" android:launchMode="singleTop">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="de.itsblue.fannyapp.MainActivity" android:label="fannyapp" android:screenOrientation="unspecified" android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@ -52,8 +52,8 @@
then android.app.splash_screen_drawable. For best results, use together with splash_screen_sticky and
use hideSplashScreen() with a fade-out animation from Qt Android Extras to hide the splash screen when you
are done populating your window with content. -->
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/screen"/>
<!--meta-data android:name="android.app.splash_screen_drawable_portrait" android:resource="@drawable/logo_portrait" />
<!--meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/screen"/>
<meta-data android:name="android.app.splash_screen_drawable_portrait" android:resource="@drawable/logo_portrait" />
<meta-data android:name="android.app.splash_screen_drawable_landscape" android:resource="@drawable/logo_landscape" />
<meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/>
<meta-data android:name="android.app.splash_screen_sticky" android:value="true"/-->
@ -83,6 +83,10 @@
</activity>
<!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices -->
<!-- Register the File Provider for document sharing -->
<provider android:name="android.support.v4.content.FileProvider" android:authorities="de.itsblue.fannyapp.fileprovider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
</provider>
</application>

View File

@ -0,0 +1,58 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0'
}
}
repositories {
google()
jcenter()
}
apply plugin: 'com.android.application'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
compile 'com.android.support:support-v4:25.3.1'
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qt5AndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
compileSdkVersion androidCompileSdkVersion.toInteger()
buildToolsVersion androidBuildToolsVersion
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qt5AndroidDir + '/res', 'res']
resources.srcDirs = ['src']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
lintOptions {
abortOnError false
}
}

Binary file not shown.

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

172
android-sources/gradlew vendored Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
android-sources/gradlew.bat vendored Normal file
View File

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
del libcrypto.so libssl.so
mklink /H libcrypto.so libcrypto.so.1.1
mklink /H libssl.so libssl.so.1.1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
del libcrypto.so libssl.so
mklink /H libcrypto.so libcrypto.so.1.1
mklink /H libssl.so libssl.so.1.1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
del libcrypto.so libssl.so
mklink /H libcrypto.so libcrypto.so.1.1
mklink /H libssl.so libssl.so.1.1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
del libcrypto.so libssl.so
mklink /H libcrypto.so libcrypto.so.1.1
mklink /H libssl.so libssl.so.1.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,25 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<array name="qt_sources">
<item>https://download.qt.io/ministro/android/qt5/qt-5.9</item>
</array>
<!-- The following is handled automatically by the deployment tool. It should
not be edited manually. -->
<array name="bundled_libs">
<!-- %%INSERT_EXTRA_LIBS%% -->
</array>
<array name="qt_libs">
<!-- %%INSERT_QT_LIBS%% -->
</array>
<array name="bundled_in_lib">
<!-- %%INSERT_BUNDLED_IN_LIB%% -->
</array>
<array name="bundled_in_assets">
<!-- %%INSERT_BUNDLED_IN_ASSETS%% -->
</array>
</resources>

View File

@ -0,0 +1,3 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="tmp_pdf_files" path="tmp_pdf_files/" />
</paths>

View File

@ -0,0 +1,39 @@
package de.itsblue.fannyapp;
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
//
public static native void fireActivityResult(int requestCode, int resultCode);
// 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) {
// 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);
}
} // class QShareActivity

View File

@ -0,0 +1,199 @@
// (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.fannyapp.fileprovider";
protected QShareUtils()
{
//Log.d("ekkescorner", "QShareUtils()");
}
// 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<ResolveInfo> 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<ResolveInfo>() {
@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<Intent> targetedIntents = new ArrayList<Intent>();
// 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;
}
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);
}
}

View File

@ -20,6 +20,7 @@ TARGET = fannyapp
ICON = shared/graphics/favicon.icns
SOURCES += \
sources/filehelper.cpp \
sources/serverconn.cpp \
sources/main.cpp \
sources/appsettings.cpp \
@ -29,6 +30,7 @@ SOURCES += \
sources/appstyle.cpp
HEADERS += \
headers/filehelper.h \
headers/serverconn.h \
headers/appsettings.h \
headers/foodplanmodel.h \
@ -68,5 +70,8 @@ ios {
DISTFILES += \
android-sources/AndroidManifest.xml \
CHANGELOG.md
android-sources/build.gradle \
CHANGELOG.md \
android-sources/src/de/itsblue/fannyapp/MainActivity.java \
android-sources/src/org/ekkescorner/utils/QShareUtils.java

50
headers/filehelper.h Normal file
View File

@ -0,0 +1,50 @@
#ifndef FILEHELPER_H
#define FILEHELPER_H
#include <QObject>
#include <QDebug>
#if defined(Q_OS_IOS)
mPlatformShareUtils = new IosShareUtils(this);
#elif defined(Q_OS_ANDROID)
#include <QtAndroid>
#include <QAndroidActivityResultReceiver>
#else
#include <QDesktopServices>
#include <QUrl>
#endif
class FileHelper : public QObject
#if defined(Q_OS_ANDROID)
, public QAndroidActivityResultReceiver
#endif
{
Q_OBJECT
public:
explicit FileHelper(QObject *parent = nullptr);
void viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId);
#if defined(Q_OS_ANDROID)
void handleActivityResult(int receiverRequestCode, int resultCode, const QAndroidJniObject &data);
void onActivityResult(int requestCode, int resultCode);
static FileHelper* getInstance();
#endif
private:
#if defined(Q_OS_IOS)
#elif defined(Q_OS_ANDROID)
void processActivityResult(int requestCode, int resultCode);
static FileHelper* mInstance;
#else
#endif
signals:
void shareEditDone(int requestCode);
void shareFinished(int requestCode);
void shareNoAppAvailable(int requestCode);
void shareError(int requestCode, QString message);
public slots:
};
#endif // FILEHELPER_H

View File

@ -29,21 +29,17 @@
#include <QDesktopServices>
#include "headers/appsettings.h"
#include "headers/filehelper.h"
#ifdef Q_OS_ANDROID
#include <QtAndroidExtras>
#endif
typedef struct strReturnData{
int status_code;
QString text;
}ReturnData_t;
class ServerConn : public QObject
{
Q_OBJECT
Q_PROPERTY(QString state READ getState NOTIFY stateChanged)
Q_PROPERTY(double downloadProgress READ getDownloadProgress NOTIFY downloadProgressChanged)
private:
QString state;
@ -51,12 +47,18 @@ private:
QString username;
QString password;
ReturnData_t senddata(QUrl serviceUrl, QUrlQuery postData);
QVariantMap senddata(QUrl serviceUrl, QUrlQuery postData, bool raw = false);
QList<int> apiVersion = {0,2,1};
FileHelper * fileHelper;
QString mDocumentsWorkPath;
double downloadProgress;
private slots:
void setState(QString state);
void updateDownloadProgress(qint64 read, qint64 total);
public:
explicit ServerConn(QObject *parent = nullptr);
@ -66,12 +68,15 @@ public slots:
Q_INVOKABLE int login(QString username, QString password, bool permanent);
Q_INVOKABLE int logout();
Q_INVOKABLE int getFoodPlan();
Q_INVOKABLE int openEventPdf(QString day);
Q_INVOKABLE int getEvents(QString day);
Q_INVOKABLE double getDownloadProgress();
Q_INVOKABLE QString getState();
signals:
void stateChanged(QString newState);
void downloadProgressChanged();
public:
QList<QStringList> m_weekplan;

View File

@ -29,7 +29,6 @@ FannyDataListView {
id: foodPlanModel
}
delegate: Button {
id: delegate

View File

@ -24,6 +24,7 @@ ListView {
id: control
property int status: -1
property var optionButtonFunction: undefined
signal refresh()
@ -56,15 +57,23 @@ ListView {
InfoArea {
id: infoArea
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: app.landscape() ? parent.width * 0.4:parent.width * 0.3
topMargin: parent.height*( status === 901 ? 0.6:0.5) - height * 0.8
}
z: 0
anchors.fill: parent
excludedCodes: [200, 902]
errorCode: control.status
optionButtonFunction: control.optionButtonFunction
}
PullRefresher{
target: control
backgroundColor: app.style.style.buttonColor
pullIndicatorColor: app.style.style.textColor
preRefreshDelay: 300
refreshPosition: height * 1.3
}
}

View File

@ -29,44 +29,48 @@ Item {
property int errorCode: -1
property var excludedCodes: []
property var optionButtonFunction: undefined
visible: !(excludedCodes.indexOf(errorCode) >= 0)
height: childrenRect.height
Column {
anchors.centerIn: parent
Rectangle {
radius: height * 0.5
width: parent.width
height: width
color: "transparent"
border.width: 5
border.color: infoArea.alertLevel > 0 ? infoArea.alertLevel > 1 ? "red":"grey" : "green"
width: parent.width * 0.8
opacity: infoArea.errorCode !== 200 && infoArea.errorCode !== (-1) ? 1:0
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
spacing: 20
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
}
radius: height * 0.5
width: app.landscape() ? infoArea.height * 0.4 : parent.width * 0.5
height: width
color: "transparent"
border.width: 5
border.color: ["green", "grey", "orange", "red"][infoArea.alertLevel]
Label {
anchors.centerIn: parent
font.pixelSize: parent.height * 0.8
text: infoArea.alertLevel > 1 ? "!":"i"
color: parent.border.color
}
Label {
anchors.centerIn: parent
font.pixelSize: parent.height * 0.8
text: infoArea.alertLevel > 1 ? "!":"i"
color: infoArea.alertLevel > 0 ? infoArea.alertLevel > 1 ? "red":"grey" : "green"
}
Label {
id: errorShortDescription
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.bottom
margins: parent.height * 0.1
}
width: app.width * 0.8
width: parent.width
wrapMode: Label.Wrap
@ -80,11 +84,9 @@ Item {
id: errorLongDescription
anchors {
horizontalCenter: parent.horizontalCenter
top: errorShortDescription.bottom
margins: parent.height * 0.1
}
width: app.width * 0.8
width: parent.width
wrapMode: Label.Wrap
@ -92,6 +94,28 @@ Item {
text: app.getErrorInfo(infoArea.errorCode)[2]
}
Button {
id: optionButton
anchors {
horizontalCenter: parent.horizontalCenter
}
visible: text !== "" && infoArea.optionButtonFunction !== undefined
text: app.getErrorInfo(infoArea.errorCode)[3]
onClicked: {
infoArea.optionButtonFunction()
}
}
Behavior on opacity {
NumberAnimation {
duration: 500
}
}
}
}

View File

@ -0,0 +1,90 @@
import QtQuick 2.0
import QtQml 2.2
Item {
id: root
width: size
height: size
property int size: 200 // The size of the circle in pixel
property real arcBegin: 0 // start arc angle in degree
property real arcEnd: 270 // end arc angle in degree
property real arcOffset: 0 // rotation
property bool isPie: false // paint a pie instead of an arc
property bool showBackground: false // a full circle as a background of the arc
property real lineWidth: 20 // width of the line
property string colorCircle: "#CC3333"
property string colorBackground: "#779933"
property alias beginAnimation: animationArcBegin.enabled
property alias endAnimation: animationArcEnd.enabled
property int animationDuration: 20
onArcBeginChanged: canvas.requestPaint()
onArcEndChanged: canvas.requestPaint()
Behavior on arcBegin {
id: animationArcBegin
enabled: true
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.InOutCubic
}
}
Behavior on arcEnd {
id: animationArcEnd
enabled: true
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.InOutCubic
}
}
Canvas {
id: canvas
anchors.fill: parent
rotation: -90 + parent.arcOffset
onPaint: {
var ctx = getContext("2d")
var x = width / 2
var y = height / 2
var start = Math.PI * (parent.arcBegin / 180)
var end = Math.PI * (parent.arcEnd / 180)
ctx.reset()
if (root.isPie) {
if (root.showBackground) {
ctx.beginPath()
ctx.fillStyle = root.colorBackground
ctx.moveTo(x, y)
ctx.arc(x, y, width / 2, 0, Math.PI * 2, false)
ctx.lineTo(x, y)
ctx.fill()
}
ctx.beginPath()
ctx.fillStyle = root.colorCircle
ctx.moveTo(x, y)
ctx.arc(x, y, width / 2, start, end, false)
ctx.lineTo(x, y)
ctx.fill()
} else {
if (root.showBackground) {
ctx.beginPath();
ctx.arc(x, y, (width / 2) - parent.lineWidth / 2, 0, Math.PI * 2, false)
ctx.lineWidth = root.lineWidth
ctx.strokeStyle = root.colorBackground
ctx.stroke()
}
ctx.beginPath();
ctx.arc(x, y, (width / 2) - parent.lineWidth / 2, start, end, false)
ctx.lineWidth = root.lineWidth
ctx.strokeStyle = root.colorCircle
ctx.stroke()
}
}
}
}

View File

@ -0,0 +1,327 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.9
import QtQuick.Controls 2.4
import QtGraphicalEffects 1.0
Item {
id: control
state: "idle"
property var target // targeted ListView
property bool autoConfigureTarget: true // should the target be automaticaly be configured?
property int postRefreshDelay: 1000 // delay after reload funcion has finished
property int preRefreshDelay: 1000 // delay before reload funcion is called
property int refreshPosition: height * 1.2 // position of the item when refreshing
property int dragOutPosition: height * 1.8 // maximum drag out
property double dragRefreshPositionMultiplier: 0.5 // 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 {
RectangularGlow {
anchors.fill: backgroundRe
scale: 0.8 * backgroundRe.scale
cornerRadius: backgroundRe.radius
color: "black"
glowRadius: 0.001
spread: 0.2
}
Rectangle {
id: backgroundRe
anchors.fill: parent
radius: width * 0.5
color: control.backgroundColor
}
}
property Component busyIndicator: BusyIndicator { running: true }
property Component pullIndicator: Canvas {
property double drawProgress: control.dragProgress
rotation: drawProgress > control.dragRefreshPositionMultiplier ? 180:0
onDrawProgressChanged: {
requestPaint()
}
onPaint: {
var ctx = getContext("2d");
var topMargin = height * 0.1
var bottomMargin = topMargin
var rightMargin = 0
var leftMargin = 0
var arrowHeight = height - topMargin - bottomMargin
var peakHeight = arrowHeight * 0.35
var peakWidth = peakHeight
var lineWidth = 2
var progress = drawProgress * 1 / control.dragRefreshPositionMultiplier > 1 ? 1 : drawProgress * 1 / control.dragRefreshPositionMultiplier
// modify all values to math the progress
arrowHeight = arrowHeight * progress
if(progress > 0.3){
peakHeight = peakHeight * (progress - 0.3) * 1/0.7
peakWidth = peakWidth * (progress - 0.3) * 1/0.7
}
else {
peakHeight = 0
peakWidth = 0
}
// clear canvas
ctx.reset()
ctx.lineWidth = lineWidth;
ctx.strokeStyle = control.pullIndicatorColor;
// middle line
ctx.moveTo(width/2, topMargin);
ctx.lineTo(width/2, arrowHeight + topMargin);
// right line
ctx.moveTo(width/2 - lineWidth * 0.3, arrowHeight + topMargin);
ctx.lineTo(width/2 + peakWidth,arrowHeight + topMargin - peakHeight);
// left line
ctx.moveTo(width/2 + lineWidth * 0.3, arrowHeight + topMargin);
ctx.lineTo(width/2 - peakWidth,arrowHeight + topMargin - peakHeight);
ctx.stroke();
}
Behavior on rotation {
NumberAnimation {
duration: 100
}
}
}
signal refreshRequested
// internal properties
property int minimumPosition: 0
property int maximumPosition: 0
property int userPosition: 0
property int position: Math.max( minimumPosition, Math.min(maximumPosition, userPosition))
height: 50
width: height
Component.onCompleted: {
if(control.autoConfigureTarget){
target.boundsBehavior = Flickable.DragOverBounds
target.boundsMovement = Flickable.StopAtBounds
}
}
function refresh() {
control.refreshRequested()
postRefreshTimer.start()
}
anchors {
top: control.target.top
horizontalCenter: control.target.horizontalCenter
topMargin: control.position - height
}
Connections {
target: control.target
onDragEnded: {
if(userPosition >= control.dragOutPosition * control.dragRefreshPositionMultiplier){
control.state = "refreshing"
preRefreshTimer.start()
}
}
}
Loader {
id: backgroundLd
anchors.fill: parent
sourceComponent: control.background
}
Loader {
id: pullIndicatorLd
anchors.centerIn: parent
height: parent.height * 0.6
width: height
rotation: 180
sourceComponent: control.pullIndicator
}
Loader {
id: busyIndicatorLd
anchors.centerIn: parent
height: parent.height * 0.7
width: height
opacity: 0
sourceComponent: control.busyIndicator
}
Timer {
id: preRefreshTimer
interval: control.preRefreshDelay <= 0 ? 1:control.preRefreshDelay
running: false
repeat: false
onTriggered: {
control.refresh()
}
}
Timer {
id: postRefreshTimer
interval: control.postRefreshDelay <= 0 ? 1:control.postRefreshDelay
running: false
repeat: false
onTriggered: {
control.state = "hidden"
}
}
Behavior on minimumPosition {
enabled: !control.target.dragging && state !== "idle"
NumberAnimation {
duration: 100
}
}
states: [
State {
name: "idle"
PropertyChanges {
target: control
minimumPosition: userPosition > maximumPosition ? maximumPosition:userPosition
userPosition: -1 / (Math.abs( (target.verticalOvershoot > 0 ? 0:target.verticalOvershoot) * 0.001 + 0.003 ) + 1 / control.dragOutPosition * 0.001) + control.dragOutPosition // Math.abs( target.verticalOvershoot )
maximumPosition: control.dragOutPosition
}
PropertyChanges {
target: pullIndicatorLd
rotation: 0
}
},
State {
name: "refreshing"
PropertyChanges {
target: control
minimumPosition: control.refreshPosition
userPosition: 0
maximumPosition: control.refreshPosition
}
PropertyChanges {
target: pullIndicatorLd
opacity: 0
}
PropertyChanges {
target: busyIndicatorLd
opacity: 1
}
},
State {
name: "hidden"
PropertyChanges {
target: control
minimumPosition: control.refreshPosition
userPosition: 0
maximumPosition: control.refreshPosition
scale: 0
}
PropertyChanges {
target: pullIndicatorLd
opacity: 0
}
PropertyChanges {
target: busyIndicatorLd
opacity: 1
}
}
]
transitions: [
Transition {
NumberAnimation {
duration: 100
properties: "rotation, opacity"
}
},
Transition {
from: "refreshing"
to: "hidden"
PauseAnimation {
duration: 200
}
NumberAnimation {
duration: 200
properties: "scale"
}
onRunningChanged: {
if(control.state === "hidden" && !running){
control.state = "idle"
}
}
},
Transition {
from: "hidden"
to: "idle"
}
]
}

View File

@ -17,8 +17,10 @@
*/
import QtQuick 2.9
import Backend 1.0
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import QtQuick.Controls.Material 2.0
import "../Components"
Page {
@ -35,56 +37,169 @@ Page {
onOpened: {}
function pdfAction() {
busyDialog.open()
serverConn.openEventPdf(day)
busyDialog.close()
}
Loader {
id: pageLoader
property string newSource: ""
property var newSourceComponent
onNewSourceChanged: {
anchors.fill: parent
sourceComponent: loadingFormComp
onNewSourceComponentChanged: {
oldItemAnimation.start()
}
anchors.fill: parent
source: "./LoadingForm.qml"
onSourceChanged: {
pageLoader.item.status = root.status
newItemAnimation.start()
onSourceComponentChanged: {
if(pageLoader.item !== null) {
pageLoader.item.status = root.status
newItemAnimation.start()
}
}
NumberAnimation {
ParallelAnimation {
id: newItemAnimation
target: pageLoader.item
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.InExpo
NumberAnimation {
target: pageLoader.item
property: "opacity"
from: 0
to: 1
duration: 300
easing.type: Easing.InExpo
}
NumberAnimation {
target: pageLoader.item
property: "scale"
from: 0.98
to: 1
duration: 300
easing.type: Easing.InExpo
}
}
NumberAnimation {
ParallelAnimation {
id: oldItemAnimation
target: pageLoader.item
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InExpo
NumberAnimation {
target: pageLoader.item
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InExpo
}
onRunningChanged: {
if(!running){
pageLoader.source = pageLoader.newSource
pageLoader.sourceComponent = pageLoader.newSourceComponent
}
}
}
Connections {
target: pageLoader.item
onRefresh: {
pageLoader.newSource = "./LoadingForm.qml"
loadTimer.start()
Component {
id: eventListComp
FannyDataListView {
id: eventList
status: 900
optionButtonFunction: function() {
busyDialog.open()
serverConn.openEventPdf(day)
busyDialog.close()
}
model: EventModel {
id: foodPlanModel
}
delegate: Button {
id: delegate
width: eventList.width
height: contentCol.height + 10
z: 100
Column {
id: contentCol
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
height: childrenRect.height + 10
spacing: 1
Label {
id: gradeLa
// label for the grade
font.bold: true
font.pixelSize: hourReplaceSubjectRoomLa.font.pixelSize * 1.5
width: parent.width - 10
wrapMode: Label.Wrap
text: grade
}
Label {
id: hourReplaceSubjectRoomLa
// label for the hour, replacement, subject and room
width: parent.width - 10
wrapMode: Label.Wrap
text: hour + ( replace === "" ? "": ( " | "
+ replace + ( subject === "" ? "": ( " | "
+ subject + ( room === "" ? "": ( " | "
+ room ) ) ) ) ) )
}
Label {
id: toTextLa
// label for the new room (to) and the additional text (text)
width: parent.width - 10
wrapMode: Label.Wrap
font.pixelSize: gradeLa.font.pixelSize
font.bold: true
visible: text !== ""
text: to !== "" && model.text !== "" ? to + " | " + model.text:model.to + model.text
}
}
}
}
}
Component {
id: loadingFormComp
LoadingForm {}
}
}
Timer {
@ -94,7 +209,47 @@ Page {
repeat: false
onTriggered: {
root.status = serverConn.getEvents(day)
pageLoader.newSource = "../Components/EventView.qml"
pageLoader.newSourceComponent = eventListComp
}
}
Popup {
id: busyDialog
parent: overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
height: contentHeight * 1.5
width: contentWidth * 1.5
contentHeight: progressCircle.height
contentWidth: progressCircle.width
Material.theme: root.Material.theme
modal: true
closePolicy: "NoAutoClose"
focus: true
ProgressCircle {
id: progressCircle
size: 50
lineWidth: 5
anchors.centerIn: parent
colorCircle: Material.theme === Material.Dark ? "#F48FB1":"#E91E63"
colorBackground: "transparent"
showBackground: true
arcBegin: 0
arcEnd: 360 * serverConn.downloadProgress
animationDuration: 0
Label {
id: progress
anchors.centerIn: parent
text: Math.round( serverConn.downloadProgress * 100 ) + "%"
}
}
}
}

View File

@ -75,14 +75,6 @@ Page {
}
}
}
Connections {
target: pageLoader.item
onRefresh: {
pageLoader.newSource = "./LoadingForm.qml"
loadTimer.start()
}
}
}
Timer {

View File

@ -140,14 +140,17 @@ Page {
rightMargin: root.width * 0.05
}
MouseArea {
ToolButton{
id: passwordHideShow
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
width: visibleIcon.width
icon.height: parent.height * 0.5
icon.width: parent.height * 0.5
icon.color: app.style.style.textColor
onClicked: {
if(state === "visible"){
@ -164,13 +167,10 @@ Page {
State {
name: "invisible"
PropertyChanges {
target: visibleIcon
scale: 0
}
PropertyChanges {
target: invisibleIcon
scale: 1
target: passwordHideShow
icon.name: "hide"
}
PropertyChanges {
target: tipasswd
echoMode: TextInput.Password
@ -179,12 +179,8 @@ Page {
State {
name: "visible"
PropertyChanges {
target: visibleIcon
scale: 1
}
PropertyChanges {
target: invisibleIcon
scale: 0
target: passwordHideShow
icon.name: "view"
}
PropertyChanges {
target: tipasswd
@ -192,39 +188,8 @@ Page {
}
}
]
Image {
id: visibleIcon
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
bottomMargin: parent.height * 0.25
topMargin: anchors.bottomMargin
}
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/graphics/icons/view.png"
}
Image {
id: invisibleIcon
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
bottomMargin: parent.height * 0.25
topMargin: anchors.bottomMargin
}
fillMode: Image.PreserveAspectFit
smooth: true
source: "qrc:/graphics/icons/hide.png"
}
}
}
CheckDelegate {

View File

@ -16,8 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick 2.2
import QtQuick.Controls 2.1
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import QtQuick.Layouts 1.3
@ -28,7 +28,7 @@ Page {
id: root
objectName: "MainPage"
/*Shortcut {
Shortcut {
sequences: ["Esc", "Back"]
enabled: formStack.depth > 1
onActivated: {
@ -36,7 +36,7 @@ Page {
formStack.pop()
}
}
}*/
}
Rectangle {
anchors.fill: parent
@ -149,6 +149,10 @@ Page {
ToolButton {
id: toolButton
enabled: !formStack.currentItem.locked
opacity: enabled ? 1:0.5
icon.name: "back"
icon.color: app.style.style.textColor
@ -182,6 +186,24 @@ Page {
}
}
ToolButton {
id: pdfToolButton
enabled: !formStack.currentItem.locked
opacity: enabled ? 1:0.5
visible: formStack.currentItem.title === "Vertretungsplan"
icon.name: "pdf"
icon.color: app.style.style.textColor
onClicked: {
if(formStack.currentItem.pdfAction !== undefined) {
formStack.currentItem.pdfAction()
}
}
}
}
Behavior on anchors.topMargin {
@ -211,3 +233,5 @@ Page {
]
}
}

View File

@ -115,31 +115,34 @@ ApplicationWindow {
var infoLevel
// 0 - ok
// 1 - info
// 2 - error
// 2 - warn
// 3 - error
var errorString
var errorDescription
var errorButtonOption = ""
switch(errorCode) {
case 0:
infoLevel = 2
infoLevel = 3
errorString = "Keine Verbindung zum Server"
errorDescription = "Bitte überprüfe deine Internetverbindung und versuche es erneut."
break
case 401:
infoLevel = 2
infoLevel = 3
errorString = "Ungültige Zugangsdaten"
errorDescription = "Der Server hat den Zugang verweigert, bitte überprüfe deine Zugangsdaten und versuche es erneut"
break
case 500:
infoLevel = 2
infoLevel = 3
errorString = "Interner Server Fehler"
errorDescription = "Scheinbar kann der Server die Anfrage im Moment nicht verarbeiten, bitte versuche es später erneut."
break
case 900:
infoLevel = 2
errorString = "Interner Verarbeitungsfehler"
errorString = "Verarbeitungsfehler"
errorDescription = "Die Daten, die vom Server übertragen wurden, konnten nicht richtig verarbeitet werden, bitte versuche es später erneut."
errorButtonOption = "Als Pdf ansehen"
break
case 901:
infoLevel = 1
@ -157,17 +160,23 @@ ApplicationWindow {
errorDescription = "Die aufgerufene Funktion ist momentan nicht verfügbar, bitte versuche es später erneut."
break
case 904:
infoLevel = 2
infoLevel = 3
errorString = "Inkompatible API"
errorDescription = "Die Version der API auf dem Server ist zu neu und kann daher nicht verarbeitet werden. Bitte aktualisiere die App auf die aktuellste Version."
errorButtonOption = "Als Pdf ansehen"
break
case 905:
infoLevel = 3
errorString = "Interner Speicherfehler"
errorDescription = "Die Pdf-Datei konnte nicht gespeichert werden."
break
default:
infoLevel = 2
infoLevel = 3
errorString = "Unerwarteter Fehler ("+errorCode+")"
errorDescription = "Unbekannter Fehler bei der Verbindung mit dem Server."
}
return([infoLevel, errorString, errorDescription])
return([infoLevel, errorString, errorDescription, errorButtonOption])
}
function landscape(){

View File

@ -16,5 +16,7 @@
<file>Forms/FilterForm.qml</file>
<file>Components/SettingsDelegate.qml</file>
<file>Components/FannyDataListView.qml</file>
<file>Components/ProgressCircle.qml</file>
<file>Components/PullRefresher.qml</file>
</qresource>
</RCC>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -72,7 +72,6 @@
<file>icons/ibmaterial/index.theme</file>
<file>icons/back.png</file>
<file>icons/backDark.png</file>
<file>icons/delete.png</file>
<file>icons/hide.png</file>
<file>icons/logoutBlack.png</file>
<file>icons/logoutRed.png</file>
@ -86,5 +85,18 @@
<file>icons/ibmaterial/20x20@2/delete.png</file>
<file>icons/ibmaterial/20x20@3/delete.png</file>
<file>icons/ibmaterial/20x20@4/delete.png</file>
<file>icons/ibmaterial/20x20/pdf.png</file>
<file>icons/ibmaterial/20x20@2/pdf.png</file>
<file>icons/ibmaterial/20x20@3/pdf.png</file>
<file>icons/ibmaterial/20x20@4/pdf.png</file>
<file>graphics/Splashscreen.png</file>
<file>icons/ibmaterial/20x20/hide.png</file>
<file>icons/ibmaterial/20x20/view.png</file>
<file>icons/ibmaterial/20x20@2/hide.png</file>
<file>icons/ibmaterial/20x20@2/view.png</file>
<file>icons/ibmaterial/20x20@3/hide.png</file>
<file>icons/ibmaterial/20x20@3/view.png</file>
<file>icons/ibmaterial/20x20@4/hide.png</file>
<file>icons/ibmaterial/20x20@4/view.png</file>
</qresource>
</RCC>

110
sources/filehelper.cpp Normal file
View File

@ -0,0 +1,110 @@
#include "../headers/filehelper.h"
#if defined(Q_OS_IOS)
#include <QUrl>
#include <QFileInfo>
#include <QDateTime>
#include <QtAndroidExtras/QAndroidJniObject>
#include <jni.h>
#endif
FileHelper::FileHelper(QObject *parent) : QObject(parent)
{
#if defined(Q_OS_IOS)
#elif defined(Q_OS_ANDROID)
mInstance = this;
#else
#endif
}
void FileHelper::viewFile(const QString &filePath, const QString &title, const QString &mimeType, const int &requestId)
{
#if defined(Q_OS_IOS)
#elif defined(Q_OS_ANDROID)
QAndroidJniObject jsPath = QAndroidJniObject::fromString(filePath);
QAndroidJniObject jsTitle = QAndroidJniObject::fromString(title);
QAndroidJniObject jsMimeType = QAndroidJniObject::fromString(mimeType);
jboolean ok = QAndroidJniObject::callStaticMethod<jboolean>("org/ekkescorner/utils/QShareUtils",
"viewFile",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)Z",
jsPath.object<jstring>(), jsTitle.object<jstring>(), jsMimeType.object<jstring>(), requestId);
if(!ok) {
qWarning() << "Unable to resolve activity from Java";
emit shareNoAppAvailable(requestId);
}
#else
Q_UNUSED(title)
Q_UNUSED(mimeType)
Q_UNUSED(requestId)
QDesktopServices::openUrl(QUrl::fromLocalFile(filePath));
#endif
}
#if defined(Q_OS_ANDROID)
const static int RESULT_OK = -1;
const static int RESULT_CANCELED = 0;
FileHelper* FileHelper::mInstance = nullptr;
FileHelper* FileHelper::getInstance()
{
if (!mInstance) {
mInstance = new FileHelper;
qWarning() << "AndroidShareUtils should be instantiated !";
}
return mInstance;
}
// used from QAndroidActivityResultReceiver
void FileHelper::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 FileHelper::onActivityResult(int requestCode, int resultCode)
{
qDebug() << "From Java Activity onActivityResult: " << requestCode << "ResultCode:" << resultCode;
processActivityResult(requestCode, resultCode);
}
void FileHelper::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) {
emit shareFinished(requestCode);
} else {
qDebug() << "wrong result code: " << resultCode << " from request: " << requestCode;
emit shareError(requestCode, tr("Share: an Error occured"));
}
}
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
JNIEXPORT void JNICALL
Java_de_itsblue_fannyapp_MainActivity_fireActivityResult(JNIEnv *env,
jobject obj,
jint requestCode,
jint resultCode)
{
Q_UNUSED (obj)
Q_UNUSED (env)
FileHelper::getInstance()->onActivityResult(requestCode, resultCode);
return;
}
#ifdef __cplusplus
}
#endif // __cplusplus
#endif //defined(Q_OS_ANDROID)

View File

@ -26,6 +26,27 @@ ServerConn::ServerConn(QObject *parent) : QObject(parent)
qDebug("+----- ServerConn constructor -----+");
pGlobalServConn = this;
this->fileHelper = new FileHelper();
connect(this->fileHelper, &FileHelper::shareError, [=](){qWarning() << "share error";});
connect(this->fileHelper, &FileHelper::shareFinished, [=](){qWarning() << "share finished";});
connect(this->fileHelper, &FileHelper::shareNoAppAvailable, [=](){qWarning() << "share no app available";});
// get local work path
#if defined (Q_OS_IOS)
QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation).value(0);
qDebug() << "iOS: QStandardPaths::DocumentsLocation: " << docLocationRoot;
#elif defined(Q_OS_ANDROID)
QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0);
#else
QString docLocationRoot = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation).value(0);
#endif
mDocumentsWorkPath = docLocationRoot.append("/tmp_pdf_files");
if (!QDir(mDocumentsWorkPath).exists()) {
if (!QDir("").mkpath(mDocumentsWorkPath)) {
pGlobalAppSettings->writeSetting("localDocPathError", "true");
}
}
// check login state
int perm = pGlobalAppSettings->loadSetting("permanent").toInt();
qDebug() << "+----- login state: " << perm << " -----+";
@ -51,9 +72,9 @@ int ServerConn::login(QString username, QString password, bool permanent)
pdata.addQueryItem("password", password);
// send the request
ReturnData_t ret = this->senddata(QUrl("http://www.fanny-leicht.de/j34/templates/g5_helium/intern/events.php"), pdata);
QVariantMap ret = this->senddata(QUrl("http://www.fanny-leicht.de/j34/templates/g5_helium/intern/events.php"), pdata);
if(ret.status_code == 200){
if(ret["status"].toInt() == 200){
// if not 200 was returned -> user data was correct
// store username and password in the class variables
this->username = username;
@ -81,7 +102,7 @@ int ServerConn::login(QString username, QString password, bool permanent)
pGlobalAppSettings->writeSetting("permanent", "0");
pGlobalAppSettings->writeSetting("username", "");
pGlobalAppSettings->writeSetting("password", "");
return(ret.status_code);
return(ret["status"].toInt());
}
}
@ -119,19 +140,19 @@ int ServerConn::getEvents(QString day)
pdata.addQueryItem("day", day);
// send the request
ReturnData_t ret = this->senddata(QUrl("https://www.fanny-leicht.de/j34/templates/g5_helium/intern/events.php"), pdata);
QVariantMap ret = this->senddata(QUrl("https://www.fanny-leicht.de/j34/templates/g5_helium/intern/events.php"), pdata);
if(ret.status_code != 200){
if(ret["status"].toInt() != 200){
// if the request didn't result in a success, clear the old events, as they are probaply incorrect and return the error code
this->m_events.clear();
if(ret.status_code == 401){
if(ret["status"].toInt() == 401){
// if the stats code is 401 -> userdata is incorrect
qDebug() << "+----- checkconn: user data is incorrect -----+";
logout();
}
return ret.status_code;
return ret["status"].toInt();
}
@ -152,7 +173,7 @@ int ServerConn::getEvents(QString day)
QStringList tmpEventHeader;
//qDebug() << jsonString;
QJsonDocument jsonFilters = QJsonDocument::fromJson(ret.text.toUtf8());
QJsonDocument jsonFilters = QJsonDocument::fromJson(ret["text"].toString().toUtf8());
// array with tghe whole response in it
QJsonObject JsonArray = jsonFilters.object();
@ -230,15 +251,68 @@ int ServerConn::getEvents(QString day)
}
// check if there is any valid data
if(tmpEvents.length() < 3){
if(tmpEvents.length() == 0 || tmpEvents.length() == 1) {
// no data was delivered at all -> the server encountered a parse error
tmpEvents.clear();
ret["status"].setValue(900);
}
else if(tmpEvents.length() == 2) {
// remove the last (in this case the second) element, as it is unnecessary (it is the legend -> not needed when there is no data)
tmpEvents.takeLast();
// set return code to 'no data' (901)
ret.status_code = 901;
ret["status"].setValue(901);
}
this->m_events = tmpEvents;
return(ret.status_code);
return(ret["status"].toInt());
}
int ServerConn::openEventPdf(QString day) {
// day: 0-today; 1-tomorrow
if(this->state != "loggedIn"){
return 401;
}
if(pGlobalAppSettings->loadSetting("localDocPathError") == "true") {
// we have no local document path to work with -> this is not going to work!
return 905;
}
// add the data to the request
QUrlQuery pdata;
pdata.addQueryItem("username", this->username);
pdata.addQueryItem("password", this->password);
pdata.addQueryItem("mode", pGlobalAppSettings->loadSetting("teacherMode") == "true" ? "1":"0");
pdata.addQueryItem("day", day);
pdata.addQueryItem("asPdf", "true");
// send the request
QVariantMap ret = this->senddata(QUrl("https://www.fanny-leicht.de/j34/templates/g5_helium/intern/events.php"), pdata, true);
if(ret["status"].toInt() != 200){
// if the request didn't result in a success, clear the old events, as they are probaply incorrect and return the error code
this->m_events.clear();
if(ret["status"].toInt() == 401){
// if the stats code is 401 -> userdata is incorrect
qDebug() << "+----- checkconn: user data is incorrect -----+";
logout();
}
return ret["status"].toInt();
}
QString path = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
QString filname = (QString(pGlobalAppSettings->loadSetting("teacherMode") == "true" ? "l":"s") + QString(day == "0" ? "heute":"morgen" ));
QFile file(mDocumentsWorkPath + "/" + filname + ".pdf");
file.remove();
file.open(QIODevice::ReadWrite);
file.write(ret["data"].toByteArray());
file.close();
this->fileHelper->viewFile(mDocumentsWorkPath + "/" + filname + ".pdf", "SMorgen", "application/pdf", 1);
return 200;
}
int ServerConn::getFoodPlan()
@ -255,18 +329,18 @@ int ServerConn::getFoodPlan()
QUrlQuery pdata;
// send the request to the server
ReturnData_t ret = this->senddata(QUrl(url), pdata);
QVariantMap ret = this->senddata(QUrl(url), pdata);
if(ret.status_code != 200){
if(ret["status"].toInt() != 200){
// if the request didn't result in a success, return the error code
// if the request failed but there is still old data available
if(!this->m_weekplan.isEmpty()){
// set the status code to 902 (old data)
ret.status_code = 902;
ret["status"].setValue(902);
}
return(ret.status_code);
return(ret["status"].toInt());
}
// list to be returned
@ -274,8 +348,8 @@ int ServerConn::getFoodPlan()
QList<QStringList> tmpWeekplan;
//qDebug() << jsonString;
QJsonDocument jsonDoc = QJsonDocument::fromJson(ret.text.toUtf8());
//qDebug() << ret.text;
QJsonDocument jsonDoc = QJsonDocument::fromJson(ret["text"].toString().toUtf8());
//qDebug() << ret["text"].toString();
// array with the whole response in it
QJsonArray foodplanDays = jsonDoc.array();
@ -329,12 +403,12 @@ int ServerConn::getFoodPlan()
return(200);
}
ReturnData_t ServerConn::senddata(QUrl serviceUrl, QUrlQuery pdata)
QVariantMap ServerConn::senddata(QUrl serviceUrl, QUrlQuery pdata, bool raw)
{
// create network manager
QNetworkAccessManager * networkManager = new QNetworkAccessManager();
ReturnData_t ret; //this is a custom type to store the return-data
QVariantMap ret; //this is a custom type to store the return-data
// Create network request
QNetworkRequest request(serviceUrl);
@ -362,22 +436,33 @@ ReturnData_t ServerConn::senddata(QUrl serviceUrl, QUrlQuery pdata)
// start the timer
timer.start(10000);
qDebug() << "+--- starting request now...";
this->updateDownloadProgress(0, 1);
reply = networkManager->post(request, pdata.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::sslErrors, this, [=](){ reply->ignoreSslErrors(); });
connect(reply, SIGNAL(downloadProgress(qint64, qint64)),
this, SLOT(updateDownloadProgress(qint64, qint64)));
// start the loop
loop.exec();
qDebug() << "+--- request finished";
if(!timer.isActive()) {
// timeout
return {{"status", 0}};
}
//get the status code
QVariant status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
ret.status_code = status_code.toInt();
ret.insert("status", status_code.toInt());
//get the full text response
ret.text = QString::fromUtf8(reply->readAll());
if(!raw) {
ret.insert("text", QString::fromUtf8(reply->readAll()));
}
else {
ret.insert("data", reply->readAll());
}
// delete the reply object
reply->deleteLater();
@ -389,6 +474,22 @@ ReturnData_t ServerConn::senddata(QUrl serviceUrl, QUrlQuery pdata)
return(ret);
}
void ServerConn::updateDownloadProgress(qint64 read, qint64 total)
{
double progress;
if(total <= 0)
progress = 0;
else
progress = (double(read) / double(total));
if(progress < 0)
progress = 0;
this->downloadProgress = progress;
emit this->downloadProgressChanged();
}
QString ServerConn::getState() {
return(this->state);
}
@ -402,6 +503,10 @@ void ServerConn::setState(QString state) {
}
}
double ServerConn::getDownloadProgress() {
return this->downloadProgress;
}
ServerConn::~ServerConn()
{
qDebug("+----- ServerConn destruktor -----+");