From 4721e6c0347c645bd0f981b2be0d1e6c564b37bd Mon Sep 17 00:00:00 2001 From: yajo10 Date: Mon, 13 Nov 2023 19:04:58 +0100 Subject: [PATCH] Initial commit --- harbour-expenditure.desktop | 20 + harbour-expenditure.pro | 48 + harbour-expenditure.pro.user | 1117 ++++++++++++++++++++++++ icons/108x108/harbour-expenditure.png | Bin 0 -> 6623 bytes icons/128x128/harbour-expenditure.png | Bin 0 -> 7939 bytes icons/172x172/harbour-expenditure.png | Bin 0 -> 10886 bytes icons/86x86/harbour-expenditure.png | Bin 0 -> 5098 bytes qml/cover/CoverPage.qml | 34 + qml/cover/harbour-expenditure.png | Bin 0 -> 17497 bytes qml/cover/harbour-expenditure.svg | 277 ++++++ qml/harbour-expenditure.qml | 409 +++++++++ qml/pages/AboutPage.qml | 91 ++ qml/pages/Banner2ButtonsChoice.qml | 153 ++++ qml/pages/BannerAddExpense.qml | 601 +++++++++++++ qml/pages/BannerAddProject.qml | 502 +++++++++++ qml/pages/CalcPage.qml | 556 ++++++++++++ qml/pages/FirstPage.qml | 445 ++++++++++ qml/pages/ScrollBar.qml | 106 +++ qml/pages/SettingsPage.qml | 456 ++++++++++ rpm/harbour-expenditure.changes.in | 18 + rpm/harbour-expenditure.changes.run.in | 25 + rpm/harbour-expenditure.spec | 67 ++ rpm/harbour-expenditure.yaml | 42 + src/File.cpp | 60 ++ src/File.h | 33 + src/harbour-expenditure.cpp | 25 + translations/harbour-expenditure-de.ts | 430 +++++++++ translations/harbour-expenditure.ts | 430 +++++++++ 28 files changed, 5945 insertions(+) create mode 100644 harbour-expenditure.desktop create mode 100644 harbour-expenditure.pro create mode 100644 harbour-expenditure.pro.user create mode 100644 icons/108x108/harbour-expenditure.png create mode 100644 icons/128x128/harbour-expenditure.png create mode 100644 icons/172x172/harbour-expenditure.png create mode 100644 icons/86x86/harbour-expenditure.png create mode 100644 qml/cover/CoverPage.qml create mode 100644 qml/cover/harbour-expenditure.png create mode 100755 qml/cover/harbour-expenditure.svg create mode 100644 qml/harbour-expenditure.qml create mode 100644 qml/pages/AboutPage.qml create mode 100644 qml/pages/Banner2ButtonsChoice.qml create mode 100644 qml/pages/BannerAddExpense.qml create mode 100644 qml/pages/BannerAddProject.qml create mode 100644 qml/pages/CalcPage.qml create mode 100644 qml/pages/FirstPage.qml create mode 100755 qml/pages/ScrollBar.qml create mode 100644 qml/pages/SettingsPage.qml create mode 100644 rpm/harbour-expenditure.changes.in create mode 100644 rpm/harbour-expenditure.changes.run.in create mode 100644 rpm/harbour-expenditure.spec create mode 100644 rpm/harbour-expenditure.yaml create mode 100644 src/File.cpp create mode 100644 src/File.h create mode 100644 src/harbour-expenditure.cpp create mode 100644 translations/harbour-expenditure-de.ts create mode 100644 translations/harbour-expenditure.ts diff --git a/harbour-expenditure.desktop b/harbour-expenditure.desktop new file mode 100644 index 0000000..8d66113 --- /dev/null +++ b/harbour-expenditure.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +Type=Application +X-Nemo-Application-Type=silica-qt5 +Icon=harbour-expenditure +Exec=harbour-expenditure +Name=Expenditure +# translation example: +# your app name in German locale (de) +# +# Remember to comment out the following line, if you do not want to use +# a different app name in German locale (de). +#Name[de]=Expenditure + +[X-Sailjail] +# Replace with your organization as a reverse domain name +OrganizationName=org.tplabs +# ApplicationName does not have to be identical to Name +ApplicationName=expenditure +# Add the required permissions here +Permissions=PublicDir;UserDirs;RemovableMedia diff --git a/harbour-expenditure.pro b/harbour-expenditure.pro new file mode 100644 index 0000000..ac729da --- /dev/null +++ b/harbour-expenditure.pro @@ -0,0 +1,48 @@ +# NOTICE: +# +# Application name defined in TARGET has a corresponding QML filename. +# If name defined in TARGET is changed, the following needs to be done +# to match new name: +# - corresponding QML filename must be changed +# - desktop icon filename must be changed +# - desktop filename must be changed +# - icon definition filename in desktop file must be changed +# - translation filenames have to be changed + +# The name of your application +TARGET = harbour-expenditure + +CONFIG += sailfishapp + +SOURCES += src/harbour-expenditure.cpp \ + src/File.cpp + +DISTFILES += qml/harbour-expenditure.qml \ + qml/cover/CoverPage.qml \ + qml/pages/AboutPage.qml \ + qml/pages/Banner2ButtonsChoice.qml \ + qml/pages/BannerAddProject.qml \ + qml/pages/CalcPage.qml \ + qml/pages/FirstPage.qml \ + qml/pages/SettingsPage.qml \ + rpm/harbour-expenditure.changes.in \ + rpm/harbour-expenditure.changes.run.in \ + rpm/harbour-expenditure.spec \ + rpm/harbour-expenditure.yaml \ + translations/*.ts \ + harbour-expenditure.desktop + +SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 + +# to disable building translations every time, comment out the +# following CONFIG line +CONFIG += sailfishapp_i18n + +# German translation is enabled as an example. If you aren't +# planning to localize your app, remember to comment out the +# following TRANSLATIONS line. And also do not forget to +# modify the localized app name in the the .desktop file. +TRANSLATIONS += translations/harbour-expenditure-de.ts + +HEADERS += \ + src/File.h diff --git a/harbour-expenditure.pro.user b/harbour-expenditure.pro.user new file mode 100644 index 0000000..6fa0a83 --- /dev/null +++ b/harbour-expenditure.pro.user @@ -0,0 +1,1117 @@ + + + + + + EnvironmentId + {8e5ac1eb-2ed7-489e-a123-f178932f820f} + + + ProjectExplorer.Project.ActiveTarget + 0 + + + ProjectExplorer.Project.EditorSettings + + true + false + true + + Cpp + + CppGlobal + + + + QmlJS + + QmlJSGlobal + + + 2 + UTF-8 + false + 4 + false + 80 + true + true + 1 + false + true + false + 0 + true + true + 0 + 8 + true + false + 1 + true + true + true + *.md, *.MD, Makefile + false + true + + + + ProjectExplorer.Project.PluginSettings + + + true + false + true + true + true + true + + + 0 + true + + true + Builtin.BuildSystem + + true + true + Builtin.DefaultTidyAndClazy + 4 + + + + true + + + + + ProjectExplorer.Project.Target.0 + + Mer.Device.Type + SailfishOS-4.4.0.58-aarch64 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-aarch64 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-aarch64.default + 1 + 0 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_aarch64_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + true + -1 + 3 + + 1 + + harbour-expenditure + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.Target.1 + + Mer.Device.Type + SailfishOS-4.4.0.58-i486 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-i486 (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-i486.default + 1 + 1 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_i486_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + true + -1 + 3 + + 1 + + harbour-expenditure (on %{Device:Name}) + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.Target.2 + + Mer.Device.Type + SailfishOS-4.4.0.58-armv7hl (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-armv7hl (in Sailfish SDK Build Engine) + SailfishOS-4.4.0.58-armv7hl.default + 1 + 1 + 0 + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Debug + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Debug + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Debug + Qt4ProjectManager.Qt4BuildConfiguration + 2 + 1 + + + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Release + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Release + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Release + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + + + 0 + false + + + + + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Profile + /home/tobias/Dokumente/Sailfish/build-harbour-expenditure-SailfishOS_4_4_0_58_armv7hl_in_Sailfish_SDK_Build_Engine-Profile + + + true + Mer.MerSdkStartStep + + + true + QtProjectManager.QMakeBuildStep + false + + + + true + Qt4ProjectManager.MakeStep + + 3 + Erstellen + Erstellen + ProjectExplorer.BuildSteps.Build + + + + true + Mer.MerSdkStartStep + + + reset + true + Mer.MerClearBuildEnvironmentStep + + + true + Qt4ProjectManager.MakeStep + clean + + 3 + Bereinigen + Bereinigen + ProjectExplorer.BuildSteps.Clean + + 2 + false + + + Profile + Qt4ProjectManager.Qt4BuildConfiguration + 0 + 1 + 0 + + 3 + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerRpmBuildStep + + + --sdk + true + QmakeProjectManager.MerRpmDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRpmDeployConfiguration + + + + + true + QmakeProjectManager.MerRpmBuildStep + + + true + QmakeProjectManager.MerRpmValidationStep + + 2 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerMb2RpmBuildConfiguration + + + + + true + QmakeProjectManager.MerPrepareTargetStep + + + true + QmakeProjectManager.MerMakeInstallBuildStep + + + --rsync + true + QmakeProjectManager.MerRsyncDeployStep + + 3 + Deployment + Deployment + ProjectExplorer.BuildSteps.Deploy + + 1 + + false + QmakeProjectManager.MerRSyncDeployConfiguration + + 3 + + dwarf + + cpu-cycles + + + 250 + + -e + cpu-cycles + --call-graph + dwarf,4096 + -F + 250 + + -F + true + 4096 + false + false + 1000 + + true + + + false + false + false + false + true + 0.01 + 10 + true + kcachegrind + 1 + + 25 + + 1 + true + false + true + + valgrind + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + + + true + /home/tobias/Dokumente/Sailfish/harbour-expenditure + false + -1 + 3 + + 1 + + harbour-expenditure + QmakeProjectManager.MerRunConfiguration:/home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + /home/tobias/Dokumente/Sailfish/harbour-expenditure/harbour-expenditure.pro + 1 + false + true + false + true + :0 + + 1 + + + + ProjectExplorer.Project.TargetCount + 3 + + + ProjectExplorer.Project.Updater.FileVersion + 22 + + + Version + 22 + + diff --git a/icons/108x108/harbour-expenditure.png b/icons/108x108/harbour-expenditure.png new file mode 100644 index 0000000000000000000000000000000000000000..c60641b69f28dda5dc1a93a40e12d50d1a066ca2 GIT binary patch literal 6623 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 pnzL$y)}i%^kD&f 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 literal 0 HcmV?d00001 diff --git a/icons/128x128/harbour-expenditure.png b/icons/128x128/harbour-expenditure.png new file mode 100644 index 0000000000000000000000000000000000000000..d17abb2c98dbc22c8da9b3db4c971134640344d3 GIT binary patch literal 7939 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

T^RQyF1oS1#?5B5 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 z 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 HQ$iB}mo6N( literal 0 HcmV?d00001 diff --git a/qml/cover/harbour-expenditure.svg b/qml/cover/harbour-expenditure.svg new file mode 100755 index 0000000..8b24cb1 --- /dev/null +++ b/qml/cover/harbour-expenditure.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qml/harbour-expenditure.qml b/qml/harbour-expenditure.qml new file mode 100644 index 0000000..9050c19 --- /dev/null +++ b/qml/harbour-expenditure.qml @@ -0,0 +1,409 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import QtQuick.LocalStorage 2.0 +import "pages" + +ApplicationWindow { + initialPage: Component { FirstPage { } } + cover: Qt.resolvedUrl("cover/CoverPage.qml") + allowedOrientations: defaultAllowedOrientations + + Item { + id: storageItem + + // general functions + function getDatabase() { + return storageItem.LocalStorage.openDatabaseSync("Bible_DB", "0.1", "BibleDatabaseComplete", 5000000); // 5 MB estimated size + } + function removeFullTable (tableName) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { tx.executeSql('DROP TABLE IF EXISTS ' + tableName) }); + } + function getTableCount (tableName, default_value) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT count(*) AS some_info FROM ' + tableName + ';'); + if (rs.rows.length > 0) { + res = rs.rows.item(0).some_info; + + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + // settings + function setSettings( setting, value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'settings_table' + '(setting TEXT UNIQUE, value TEXT)'); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'settings_table' + ' VALUES (?,?);', [setting,value]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getSettings( setting, default_value ) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT value FROM '+ 'settings_table' +' WHERE setting=?;', [setting]); + if (rs.rows.length > 0) { + res = rs.rows.item(0).value; + } else { + res = default_value; + } + if (res === null) { + res = default_value + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + //console.log(setting + " = " + res) + return res + } + + // all projects available + function setProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'projects_table' + ' VALUES (?,?,?,?,?,?);', [project_id_timestamp, + project_name, + project_members, + project_recent_payer_boolarray, + project_recent_beneficiaries_boolarray, + project_base_currency ]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateProject ( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('UPDATE projects_table' + + ' SET project_name="' + project_name + + '", project_members="' + project_members + + '", project_recent_payer_boolarray="' + project_recent_payer_boolarray + + '", project_recent_beneficiaries_boolarray="' + project_recent_beneficiaries_boolarray + + '", project_base_currency="' + project_base_currency + + '" WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateField_Project ( project_id_timestamp, field_name, new_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('UPDATE projects_table SET ' + field_name + '="' + new_value + '" WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteProject (project_id_timestamp) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'projects_table' + ' (project_id_timestamp TEXT, + project_name TEXT, + project_members TEXT, + project_recent_payer_boolarray TEXT, + project_recent_beneficiaries_boolarray TEXT, + project_base_currency TEXT)' ); + var rs = tx.executeSql('DELETE FROM ' + 'projects_table WHERE project_id_timestamp=' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + removeFullTable("table_" + project_id_timestamp) + return res; + } + function getAllProjects( default_value ) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM '+ 'projects_table;') + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push([rs.rows.item(i).project_id_timestamp, + rs.rows.item(i).project_name, + rs.rows.item(i).project_members, + rs.rows.item(i).project_recent_payer_boolarray, + rs.rows.item(i).project_recent_beneficiaries_boolarray, + rs.rows.item(i).project_base_currency, + ]) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + // all exchange rates used + function countExchangeRateOccurances (exchange_rate_currency, default_value) { + var db = getDatabase(); + var res=""; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT count(*) AS some_info FROM exchange_rates_table WHERE exchange_rate_currency=?;', [exchange_rate_currency]); + if (rs.rows.length > 0) { + res = rs.rows.item(0).some_info; + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + function setExchangeRate( exchange_rate_currency, exchange_rate_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 'exchange_rates_table' + ' (exchange_rate_currency TEXT, exchange_rate_value TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO ' + 'exchange_rates_table' + ' VALUES (?,?);', [exchange_rate_currency, exchange_rate_value ]); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function updateExchangeRate( exchange_rate_currency, exchange_rate_value ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS exchange_rates_table (exchange_rate_currency TEXT, exchange_rate_value TEXT)'); + var rs = tx.executeSql('UPDATE exchange_rates_table SET exchange_rate_value="' + exchange_rate_value + '" WHERE exchange_rate_currency="' + exchange_rate_currency + '";'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getExchangeRate(exchange_rate_currency, default_value) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM '+ 'exchange_rates_table' +' WHERE exchange_rate_currency=?;', [exchange_rate_currency]); + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push(rs.rows.item(i).exchange_rate_value) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + + + // all expenes in current project + function setExpense( project_name_table, id_unixtime_created, date_time, expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_name_table + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('INSERT OR REPLACE INTO table_' + project_name_table + ' VALUES (?,?,?,?,?,?,?,?);', [ id_unixtime_created, + date_time, + expense_name, + expense_sum, + expense_currency, + expense_info, + expense_payer, + expense_members ]); + if (rs.rowsAffected > 0) { + res = "OK"; + //console.log("project info found and updated") + } else { + res = "Error"; + } + } + ); + return res; + } + function updateExpense ( project_name_table, id_unixtime_created, date_time, expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members ) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_name_table + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('UPDATE table_' + project_name_table + + ' SET date_time="' + date_time + + '", expense_name="' + expense_name + + '", expense_sum="' + expense_sum + + '", expense_currency="' + expense_currency + + '", expense_info="' + expense_info + + '", expense_payer="' + expense_payer + + '", expense_members="' + expense_members + + '" WHERE id_unixtime_created=' + id_unixtime_created + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteExpense (project_id_timestamp, id_unixtime_created) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_id_timestamp + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + //var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ';'); + var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ' WHERE id_unixtime_created=' + id_unixtime_created + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function deleteAllExpenses (project_id_timestamp) { + var db = getDatabase(); + var res = ""; + db.transaction(function(tx) { + tx.executeSql('CREATE TABLE IF NOT EXISTS table_' + project_id_timestamp + ' (id_unixtime_created TEXT, + date_time TEXT, + expense_name TEXT, + expense_sum TEXT, + expense_currency TEXT, + expense_info TEXT, + expense_payer TEXT, + expense_members TEXT)' ); + var rs = tx.executeSql('DELETE FROM table_' + project_id_timestamp + ';'); + if (rs.rowsAffected > 0) { + res = "OK"; + } else { + res = "Error"; + } + } + ); + return res; + } + function getAllExpenses( project_name_table, default_value ) { + var db = getDatabase(); + var res=[]; + try { + db.transaction(function(tx) { + var rs = tx.executeSql('SELECT * FROM table_'+ project_name_table + ';'); + if (rs.rows.length > 0) { + for (var i = 0; i < rs.rows.length; i++) { + res.push([rs.rows.item(i).id_unixtime_created, + rs.rows.item(i).date_time, + rs.rows.item(i).expense_name, + rs.rows.item(i).expense_sum, + rs.rows.item(i).expense_currency, + rs.rows.item(i).expense_info, + rs.rows.item(i).expense_payer, + rs.rows.item(i).expense_members, + ]) + } + } else { + res = default_value; + } + }) + } catch (err) { + //console.log("Database " + err); + res = default_value; + }; + return res + } + } + +} diff --git a/qml/pages/AboutPage.qml b/qml/pages/AboutPage.qml new file mode 100644 index 0000000..08b95f2 --- /dev/null +++ b/qml/pages/AboutPage.qml @@ -0,0 +1,91 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Page { + id: page + allowedOrientations: Orientation.Portrait //All + + SilicaFlickable { + id: listView + anchors.fill: parent + contentHeight: idColumn.height // Tell SilicaFlickable the height of its content. + + VerticalScrollDecorator {} + + Column { + id: idColumn + x: Theme.paddingLarge + width: parent.width - 2*x + + Label { + width: parent.width + height: Theme.itemSizeLarge + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeLarge + color: Theme.primaryColor + text: qsTr("Expenditure") + } + Item { + width: parent.width + height: Theme.paddingLarge + } + Image { + width: parent.width + height: Theme.itemSizeHuge + source: "../cover/harbour-expenditure.png" + sourceSize.width: height + sourceSize.height: height + fillMode: Image.PreserveAspectFit + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups.") + + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Thanksgiving, feedback and support is always welcome.") + bottomPadding: Theme.paddingLarge * 2 + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + text: qsTr("Troubleshooting:") + + "\n" + qsTr("In case of any database error tap 10x on the word 'Settings' for cleanup options.") + bottomPadding: Theme.paddingLarge * 2 + } + Label { + width: parent.width + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + wrapMode: Text.Wrap + text: qsTr("Contact:") + + "\n" + qsTr("Copyright © 2022 Tobias Planitzer") + + "\n" + ("tp.labs@protonmail.com") + + "\n" + qsTr("License: GPL v3") + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + } + } // end Silica Flickable +} diff --git a/qml/pages/Banner2ButtonsChoice.qml b/qml/pages/Banner2ButtonsChoice.qml new file mode 100644 index 0000000..a0c4870 --- /dev/null +++ b/qml/pages/Banner2ButtonsChoice.qml @@ -0,0 +1,153 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + + // UI variables + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property string headlineInfoText : "" + property string detailedInfoText : "" + property string otherInfoText : "" + property string filePath_Action : "" + + Behavior on opacity { + FadeAnimator {} + } + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectProject + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + text: headlineInfoText + bottomPadding: Theme.paddingLarge + } + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + text: detailedInfoText + bottomPadding: Theme.paddingLarge + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Button { + id: idButton1 + width: parent.width /2 - Theme.paddingLarge /2 + onClicked: { + restoreProjectExpenses(filePath_Action, "replace") + //backNavigationBlocked_deletedAddedProject = true + hide() + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idButton2 + width: parent.width /2 - Theme.paddingLarge /2 + height: idButton1.height + onClicked: { + restoreProjectExpenses(filePath_Action, "merge") + //backNavigationBlocked_deletedAddedProject = true + hide() + } + } + } + + Label { + x: Theme.paddingLarge + width: parent.width - 2*x + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: otherInfoText + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + } + + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectProject.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + function notify( color, upperMargin, headText, bodyText, otherText, choiceText_1, choiceText_2, filePath ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectProject.color = color + } else { + idBackgroundRectProject.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectProject.anchors.topMargin = upperMargin + } else { + idBackgroundRectProject.anchors.topMargin = 0 + } + + // set texts + headlineInfoText = headText + detailedInfoText = bodyText + otherInfoText = otherText + idButton1.text = choiceText_1 + idButton2.text = choiceText_2 + filePath_Action = filePath + + // show banner overlay + popup.opacity = 1.0 + } + + function hide() { + popup.opacity = 0.0 + } + +} + diff --git a/qml/pages/BannerAddExpense.qml b/qml/pages/BannerAddExpense.qml new file mode 100644 index 0000000..bf9cc82 --- /dev/null +++ b/qml/pages/BannerAddExpense.qml @@ -0,0 +1,601 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + onOpacityChanged: { + //if needed + } + + + + // UI variables + property var activeProjectID + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property int amountBeneficiaries : 0 + property string modeEdit : "new" + property date currentDate + property date currentTime + property double editedTimeStamp // unixtime, can be edited, is NOT entry creation timestamp + property double createdTimeStamp + property bool dateTimeManuallyChanged : false + + + // suppress blend to main window on this overlay, e.g. for context menu ... + // BUG: creates problems with _selectOrientation for context menus larger than 5 entries + property alias __silica_applicationwindow_instance: fakeApplicationWindow + Item { + id: fakeApplicationWindow + // suppresses warnings by context menu + property var _dimScreen + property var _undim + function _undim() {} + function _dimScreen() {} + } + Behavior on opacity { + FadeAnimator {} + } + + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectExpenses + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge * 2 + + Column { + id: idColumnAddDate + width: parent.width /3*2 - Theme.paddingLarge /2 + + Label { + width: parent.width + text: currentDate.toLocaleDateString(Qt.locale(), "dd. MMMM yyyy") + color: Theme.highlightColor + + MouseArea { + anchors.fill: parent + onClicked: { + unFocusTextFields() + var dialog = pageStack.push(datePickerComponent, { + date: currentDate // preset picker to todays date + } ) + dialog.accepted.connect( function () { + currentDate = (dialog.date) + editedTimeStamp = Number((combineDateAndTime(currentDate, currentTime)).getTime()) + dateTimeManuallyChanged = true + } ) + } + } + } + Label { + width: parent.width + text: currentTime.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) + color: Theme.highlightColor + + MouseArea { + anchors.fill: parent + onClicked: { + unFocusTextFields() + var dialog = pageStack.push(timePickerComponent, { + hour: currentTime.getHours(), // preset picker to current time + minute: currentTime.getMinutes(), + hourMode: 1 + } ) + dialog.accepted.connect( function () { + currentTime = new Date ( dialog.time) + editedTimeStamp = Number((combineDateAndTime(currentDate, currentTime)).getTime()) + dateTimeManuallyChanged = true + } ) + } + } + } + /* + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: "create_" + createdTimeStamp + } + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: "edit_" + editedTimeStamp + } + */ + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idLabelHeaderAdd2 + enabled: (idTextfieldItem.length > 0) && (amountBeneficiaries > 0) + width: parent.width /3 - Theme.paddingLarge /2 + height: idColumnAddDate.height + text: (modeEdit === "new") ? qsTr("Add") : qsTr("Save") + onClicked: { + addEditExpense() + } + } + } + TextField { + id: idTextfieldItem + width: page.width + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("expense") + } + } + Row { + width: parent.width + + TextField { + id: idTextfieldPrice + width: parent.width /3 + parent.width /6 + textRightMargin: 0 + inputMethodHints: Qt.ImhFormattedNumbersOnly //use "Qt.ImhDigitsOnly" for INT + text: Number("0").toFixed(2) + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + text = text.replace(",", ".") + text = Number(text).toFixed(2) + if (focus) { + selectAll() + } + } + + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("price") + } + } + Item { + width: parent.width / 6 + height: 1 + } + TextField { + id: idTextfieldCurrency + width: parent.width /3 + textLeftMargin: 0 + horizontalAlignment: TextInput.AlignRight + acceptableInput: text.length > 0 + EnterKey.enabled: text.length >= 0 + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length === 0) { + text = recentlyUsedCurrency + } + if (focus) { + selectAll() + } + } + + Label { + anchors.right: parent.right + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("currency") + } + } + } + TextField { + id: idTextfieldInfo + width: page.width + //acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("info") + } + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge * 2 + bottomPadding: Theme.paddingMedium + + Label { + width: parent.width / 2 - Theme.paddingLarge/2 + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("payment by") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width / 2 - Theme.paddingLarge/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("beneficiary") + } + } + Column { + x: Theme.paddingLarge + width: parent.width - 2*x + + Repeater { + model: listModel_activeProjectMembers + delegate: Row { + id: idtestcolumn + width: parent.width + + Label { + id: idLabelPayerName + width: parent.width / 3*2 - Theme.paddingLarge/2 + height: Theme.iconSizeSmallPlus + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + font.bold: member_isPayer === "true" + color: (member_isPayer === "true") ? Theme.primaryColor : Theme.highlightColor + text: member_name + + MouseArea { + anchors.fill: parent + onClicked: { + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + listModel_activeProjectMembers.setProperty(i, "member_isPayer", "false") + } + member_isPayer="true" + } + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Item { + width: parent.width / 3 - Theme.paddingLarge/2 + height: parent.height + + Icon { + anchors.right: parent.right + height: parent.height + width: height + color: (member_isPayer==="true") ? Theme.primaryColor : Theme.highlightColor + source: (member_isBeneficiary==="true") ? "image://theme/icon-m-accept?" : "" + + Rectangle { + z: -1 + anchors.centerIn: parent + width: parent.width - Theme.paddingSmall + height: width + color: "transparent" + border.width: (member_isPayer==="true") ? 2 : 1 + border.color: Theme.secondaryColor + radius: width/4 + } + + MouseArea { + anchors.fill: parent + anchors.leftMargin: -Theme.paddingLarge + anchors.rightMargin: -Theme.paddingLarge + onClicked: { + (member_isBeneficiary==="true") ? (member_isBeneficiary="false") : (member_isBeneficiary="true") + amountBeneficiaries = 0 + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + amountBeneficiaries += 1 + } + } + } + } + } + } + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall / 2 + } + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectExpenses.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + + function notify( color, upperMargin, modeEditNew, activeProjectID_unixtime, expense_ID_created ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectExpenses.color = color + } + else { + idBackgroundRectExpenses.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectExpenses.anchors.topMargin = upperMargin + } + else { + idBackgroundRectExpenses.anchors.topMargin = 0 + } + + // project settings + activeProjectID = activeProjectID_unixtime + modeEdit = modeEditNew + + // reset time and date to current time and date + if (modeEditNew === "new") { + idTextfieldItem.text = "" + idTextfieldPrice.text = "0" + idTextfieldCurrency.text = recentlyUsedCurrency + idTextfieldInfo.text = "" + currentDate = new Date() + currentTime = new Date() + createdTimeStamp = Number(new Date().getTime()) + editedTimeStamp = Number(new Date().getTime()) + } else { // modeEditNew === "edit" + //console.log("editing " + expense_ID_created) + for (var i = 0; i < listModel_activeProjectExpenses.count ; i++) { + if (Number(expense_ID_created) === Number(listModel_activeProjectExpenses.get(i).id_unixtime_created)) { + idTextfieldItem.text = listModel_activeProjectExpenses.get(i).expense_name + idTextfieldPrice.text = listModel_activeProjectExpenses.get(i).expense_sum + idTextfieldCurrency.text = listModel_activeProjectExpenses.get(i).expense_currency + idTextfieldInfo.text = listModel_activeProjectExpenses.get(i).expense_info + var olderDateTime = Number(listModel_activeProjectExpenses.get(i).date_time) + currentDate = new Date(olderDateTime) + currentTime = new Date (olderDateTime) + createdTimeStamp = Number(listModel_activeProjectExpenses.get(i).id_unixtime_created) + editedTimeStamp = Number(listModel_activeProjectExpenses.get(i).date_time) + } + } + } + + // remember last beneficiaries and payer in "new" mode, load expense beneficiaries and payer in "edit" mode + amountBeneficiaries = 0 + if (modeEditNew === "new") { + + // count beneficiaries + for (i = 0; i < listModel_activeProjectMembers.count ; i++) { + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + amountBeneficiaries += 1 + } + } + + // use last used project specific beneficiaries and payers settings + for (var j = 0; j < listModel_allProjects.count ; j++) { + if ( Number(listModel_allProjects.get(j).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + var activeProjectMembersArray = (listModel_allProjects.get(j).project_members).split(" ||| ") + var activeProjectRecentPayerArray = (listModel_allProjects.get(j).project_recent_payer_boolarray).split(" ||| ") + var activeProjectRecentBeneficiariesArray = (listModel_allProjects.get(j).project_recent_beneficiaries_boolarray).split(" ||| ") + for (var s = 0; s < activeProjectMembersArray.length ; s++) { + listModel_activeProjectMembers.set( s, { + "member_isBeneficiary" : activeProjectRecentBeneficiariesArray[s], + "member_isPayer" : activeProjectRecentPayerArray[s], + }) + } + } + } + } else { // "edit" mode + + // find item in expenses list for editing + for (var k = 0; k < listModel_activeProjectExpenses.count ; k++) { + if (Number(expense_ID_created) === Number(listModel_activeProjectExpenses.get(k).id_unixtime_created)) { + var editedBeneficiariesList = (listModel_activeProjectExpenses.get(k).expense_members).split(" ||| ") + var editPayersList =(listModel_activeProjectExpenses.get(k).expense_payer).split(" ||| ") + + for (var l = 0; l < listModel_activeProjectMembers.count; l++) { + listModel_activeProjectMembers.setProperty(l, "member_isBeneficiary", "false") + listModel_activeProjectMembers.setProperty(l, "member_isPayer", "false") + + // mark beneficiaries and get amount + for (var m = 0; m < editedBeneficiariesList.length; m++) { + if (listModel_activeProjectMembers.get(l).member_name === editedBeneficiariesList[m]) { + listModel_activeProjectMembers.setProperty(l, "member_isBeneficiary", "true") + amountBeneficiaries += 1 + } + } + // mark payer + for (m = 0; m < editPayersList.length; m++) { + if (listModel_activeProjectMembers.get(l).member_name === editPayersList[m]) { + listModel_activeProjectMembers.setProperty(l, "member_isPayer", "true") + } + } + } + } + } + } + //console.log(amountBeneficiaries) + + // show banner overlay + popup.opacity = 1.0 + + // focus on expense text searchField + if (modeEditNew === "new") { + idTextfieldItem.forceActiveFocus() + } + } + + function hide() { + unFocusTextFields() + popup.opacity = 0.0 // make invisible + + // clear all fields + idTextfieldItem.text = "" + idTextfieldPrice.text = "0" + idTextfieldCurrency.text = recentlyUsedCurrency + idTextfieldInfo.text = "" + //idButtonAddExpense.visible = true + } + + function unFocusTextFields() { + idTextfieldItem.focus = false + idTextfieldPrice.focus = false + idTextfieldCurrency.focus = false + idTextfieldInfo.focus = false + } + + function combineDateAndTime(date, time) { + // warning: slice necessary to avoid singele digit outputs which can not be used in combined call + var year = date.getFullYear(); + var month = ('0' + (date.getMonth() + 1)).slice(-2); // Jan is 0, dec is 11 + var day = ('0' + date.getDate()).slice(-2) + //var month = date.getMonth() // only give one digit outputs <10, which causes errors later + //var day = date.getDate(); // only gives one digit outputs <10, which causes errors later + var dateString = year + '-' + month + '-' + day; + var hours = ('0' + time.getHours()).slice(-2) + var minutes = ('0' + time.getMinutes()).slice(-2) + var timeString = hours + ':' + minutes + ':00'; + //var timeString = time.getHours() + ':' + time.getMinutes() + ':00'; + //console.log(dateString) + //console.log(timeString) + var combined = Date.fromLocaleString(Qt.locale(), dateString + ' ' + timeString, "yyyy-MM-dd hh:mm:ss") + //console.log(combined) + return combined; + } + + function addEditExpense() { + var project_name_table = activeProjectID.toString() + var id_unixtime_created = createdTimeStamp // time of entry creation, does not change, serves as unique expense_ID + var date_time = editedTimeStamp // new or edited time of expense + var expense_name = idTextfieldItem.text + var expense_sum = idTextfieldPrice.text + var expense_currency = idTextfieldCurrency.text + var expense_info = idTextfieldInfo.text + var expense_members = "" + var project_recent_payer_boolarray = "" + var project_recent_beneficiaries_boolarray = "" + for (var i = 0; i < listModel_activeProjectMembers.count ; i++) { + project_recent_payer_boolarray += " ||| " + listModel_activeProjectMembers.get(i).member_isPayer + project_recent_beneficiaries_boolarray += " ||| " + listModel_activeProjectMembers.get(i).member_isBeneficiary + if (listModel_activeProjectMembers.get(i).member_isPayer === "true") { + var expense_payer = listModel_activeProjectMembers.get(i).member_name + + } + if (listModel_activeProjectMembers.get(i).member_isBeneficiary === "true") { + expense_members += " ||| " + listModel_activeProjectMembers.get(i).member_name + } + } + project_recent_payer_boolarray = project_recent_payer_boolarray.replace(" ||| ", "") + project_recent_beneficiaries_boolarray = project_recent_beneficiaries_boolarray.replace(" ||| ", "") + expense_members = expense_members.replace(" ||| ", "") + //console.log("table_name= " + project_name_table) + //console.log(id_unixtime_created + ", " + date_time + ", " + expense_name + ", " + expense_sum + ", " + expense_currency + ", " + expense_info + ", " + expense_payer + ", " + expense_members) + + // update listmodel expenses and store in DB + if (modeEdit === "new") { + listModel_activeProjectExpenses.append({ + id_unixtime_created : Number(id_unixtime_created).toFixed(0), + date_time : Number(date_time).toFixed(0), + expense_name : expense_name, + expense_sum : Number(expense_sum).toFixed(2), + expense_currency : expense_currency, + expense_info : expense_info, + expense_payer : expense_payer, + expense_members : expense_members, + }) + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + } else { //modeEdit === "edit" + for (var j = 0; j < listModel_activeProjectExpenses.count ; j++) { + if (id_unixtime_created === Number(listModel_activeProjectExpenses.get(j).id_unixtime_created)) { + listModel_activeProjectExpenses.set(j, { + id_unixtime_created : Number(id_unixtime_created).toFixed(0), + date_time : Number(date_time).toFixed(0), + expense_name : expense_name, + expense_sum : Number(expense_sum).toFixed(2), + expense_currency : expense_currency, + expense_info : expense_info, + expense_payer : expense_payer, + expense_members : expense_members, + }) + } + } + storageItem.updateExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + // if dates got changed: also sort expenses list + if (dateTimeManuallyChanged) { + listModel_activeProjectExpenses.quick_sort() + dateTimeManuallyChanged = false + } + } + + //remember recently used currency + recentlyUsedCurrency = expense_currency + storageItem.setSettings("recentlyUsedCurrency", recentlyUsedCurrency) + + // update allProject_Listmodel and DB for recent_beneficiaries and recent_payer in case of "new" entry + if (modeEdit === "new") { + for (var k = 0; k < listModel_allProjects.count ; k++) { + if ( Number(listModel_allProjects.get(k).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + listModel_allProjects.set(k, { + "project_recent_payer_boolarray" : project_recent_payer_boolarray , + "project_recent_beneficiaries_boolarray" : project_recent_beneficiaries_boolarray, + }) + } + } + storageItem.updateField_Project(activeProjectID_unixtime, "project_recent_payer_boolarray", project_recent_payer_boolarray) + storageItem.updateField_Project(activeProjectID_unixtime, "project_recent_beneficiaries_boolarray", project_recent_beneficiaries_boolarray) + } + + // finally hide popup banner + hide() + } +} + diff --git a/qml/pages/BannerAddProject.qml b/qml/pages/BannerAddProject.qml new file mode 100644 index 0000000..868c68d --- /dev/null +++ b/qml/pages/BannerAddProject.qml @@ -0,0 +1,502 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +MouseArea { + id: popup + z: 10 + width: parent.width + height: parent.height + visible: opacity > 0 + opacity: 0.0 + onClicked: { + hide() + } + onOpacityChanged: { + //if needed + } + + // UI variables + property var hideBackColor : Theme.rgba(Theme.overlayBackgroundColor, 0.9) + property int amountBeneficiaries : listModel_activeProjectMembersTEMP.count + property string modeEdit : "new" + property real editItemIndex : -1 // -1=new, otherwise gives index of list + + property int tempProjectListIndex + property bool showTextfieldMemberName : false + property string timeStamp : ((new Date).getTime()).toString() // creates unix timestamp + + property string backupFilePath : "" + + // suppress blend to main window on this overlay, e.g. for context menu ... + // BUG: creates problems with _selectOrientation for context menus larger than 5 entries + property alias __silica_applicationwindow_instance: fakeApplicationWindow + Item { + id: fakeApplicationWindow + // suppresses warnings by context menu + property var _dimScreen + property var _undim + function _undim() {} + function _dimScreen() {} + } + Behavior on opacity { + FadeAnimator {} + } + ListModel { + id: listModel_activeProjectMembersTEMP + } + RemorsePopup { + z: 10 + id: remorse_deleteProject + } + + + + Rectangle { + anchors.fill: parent + color: hideBackColor + onColorChanged: opacity = 4 + + Rectangle { + id: idBackgroundRectProject + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: parent.width + height: parent.height - anchors.topMargin - Theme.paddingLarge + radius: Theme.paddingLarge + + SilicaFlickable { + anchors.fill: parent + contentHeight: addExpenseColumn.height + clip: true + + Column { + id: addExpenseColumn + width: parent.width + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + Column { + id: idColumnAddProject + width: parent.width /3*2 - Theme.paddingLarge /2 + + Label { + width: parent.width + text: qsTr("Project") + } + Label { + width: parent.width + font.pixelSize: Theme.fontSizeTiny + text: (modeEdit === "new") ? (qsTr("create")) : (qsTr("edit")) + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + Button { + id: idLabelHeaderAdd2 + enabled: (idTextfieldProjectname.length > 0) && (amountBeneficiaries > 0) + width: parent.width /3 - Theme.paddingLarge /2 + height: idColumnAddProject.height + text: (modeEdit === "new") ? qsTr("Add") : qsTr("Save") + onClicked: { + addProjectDB() + } + } + } + Row { + width: parent.width + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + TextField { + id: idTextfieldProjectname + width: parent.width /3 * 2 - Theme.paddingLarge + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeMedium + EnterKey.onClicked: { + focus = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("name") + } + } + Item { + width: Theme.paddingLarge + height: 1 + } + TextField { + id: idTextfieldCurrencyProject + width: parent.width /3 + textLeftMargin: 0 + horizontalAlignment: TextInput.AlignRight + acceptableInput: text.length > 0 + EnterKey.enabled: text.length >= 0 + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length === 0) { + text = recentlyUsedCurrency + } + if (focus) { + selectAll() + } + } + + Label { + anchors.right: parent.right + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + color: Theme.secondaryColor + text: qsTr("base currency") + } + } + } + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + topPadding: Theme.paddingLarge + + Label { + x: Theme.paddingLarge + width: parent.width/ 3*2 - Theme.paddingLarge / 2 + height: idAddMemberButton.height + verticalAlignment: Text.AlignVCenter + font.pixelSize: Theme.fontSizeMedium + text: qsTr("Members") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Item { + id: idAddMemberButton + width: parent.width / 3 - Theme.paddingLarge/2 + height: Theme.iconSizeMedium + + Icon { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: parent.height + width: height + source: !showTextfieldMemberName ? "image://theme/icon-m-add?" : "image://theme/icon-m-clear?" + } + MouseArea { + anchors.fill: parent + onClicked: { + showTextfieldMemberName ? showTextfieldMemberName=false : showTextfieldMemberName=true + if (showTextfieldMemberName) { + // clear field and set for new member + editItemIndex = -1 // -1=add new member + idTextfieldAddMember.text = "" + idTextfieldAddMember.forceActiveFocus() + } else { + idTextfieldAddMember.text = "" + idTextfieldAddMember.focus = false + } + } + } + } + } + Column { + visible: !showTextfieldMemberName + width: parent.width + + Repeater { + model: listModel_activeProjectMembersTEMP + delegate: ListItem { + contentHeight: Theme.itemSizeExtraSmall + menu: ContextMenu { + MenuItem { + text: qsTr("rename") + onClicked: { + showTextfieldMemberName ? showTextfieldMemberName=false : showTextfieldMemberName=true + editItemIndex = index + idTextfieldAddMember.text = member_name + idTextfieldAddMember.forceActiveFocus() + } + } + MenuItem { + text: qsTr("remove") + onClicked: { + listModel_activeProjectMembersTEMP.remove(index) + } + } + } + + Row { + x: Theme.paddingLarge + width: parent.width - 2*x + height: parent.height + + Label { + width: parent.width + height: parent.height + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + text: member_name + } + } + } + } + } + Row { + width: parent.width + visible: showTextfieldMemberName + topPadding: Theme.paddingMedium + + TextField { + id: idTextfieldAddMember + width: parent.width // 3*2 - Theme.paddingLarge + acceptableInput: text.length < 255 + font.pixelSize: Theme.fontSizeSmall + onActiveFocusChanged: { + if (!focus) { + showTextfieldMemberName = false + idTextfieldAddMember.text = "" + } + } + EnterKey.onClicked: { + if (text.length > 0) { + if (editItemIndex === -1) { // -1=add new member + listModel_activeProjectMembersTEMP.append({ member_name : idTextfieldAddMember.text, + member_isBeneficiary : true, + member_isPayer : false, + }) + } else { // "edit" existing member name by index in listmodel + listModel_activeProjectMembersTEMP.setProperty(editItemIndex, "member_name", idTextfieldAddMember.text) + } + } + focus = false + showTextfieldMemberName = false + } + Label { + anchors.top: parent.bottom + anchors.topMargin: Theme.paddingSmall + font.pixelSize: Theme.fontSizeExtraSmall + text: qsTr("name") + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall + } + Row { + width: parent.width + visible: (modeEdit === "edit") && (listModel_allProjects.count > 0) // make sure there is always one project left once created + spacing: Theme.paddingLarge + leftPadding: Theme.paddingLarge + + Button { + id: idLabelDeleteProject + width: parent.width /3 - parent.spacing * 1.5 + height: idColumnAddProject.height + color: Theme.errorColor + text: (Number(activeProjectID_unixtime) != Number(timeStamp)) ? qsTr("Delete") : qsTr("Reset") + onClicked: { + if (Number(activeProjectID_unixtime) != Number(timeStamp)) { // if it is not the active project, delete it + remorse_deleteProject.execute(qsTr("Delete this project?"), function() { + deleteProject() + }) + } else { // if active project + remorse_deleteProject.execute(qsTr("Clear all transactions?"), function() { + clearProject() + }) + } + } + } + Button { + id: idLabelBackupProject + width: parent.width /3 - parent.spacing * 1.5 + height: idColumnAddProject.height + text: qsTr("Backup") + onClicked: { + // hide() // ToDo: maybe close this popup? + pageStack.push(idFolderPickerPage) + } + } + Button { + id: idLabelRestoreProject + width: parent.width /3 - parent.spacing + height: idColumnAddProject.height + text: qsTr("Restore") + onClicked: { + // hide() // ToDo: maybe close this popup? + pageStack.push(idFilePickerPage) + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall / 2 + } + } + } + } + } + Icon { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: idBackgroundRectProject.anchors.topMargin / 2 - height/2 + source: "image://theme/icon-splus-cancel?" + opacity: 1 + } + + + function notify( color, upperMargin, modeEditNew, indexCurrentProject ) { + // color settings + if (color && (typeof(color) != "undefined")) { + idBackgroundRectProject.color = color + } + else { + idBackgroundRectProject.color = Theme.rgba(Theme.highlightBackgroundColor, 0.9) + } + + // position settings + if (upperMargin && (typeof(upperMargin) != "undefined")) { + idBackgroundRectProject.anchors.topMargin = upperMargin + } + else { + idBackgroundRectProject.anchors.topMargin = 0 + } + + // adjust input fields + tempProjectListIndex = indexCurrentProject + listModel_activeProjectMembersTEMP.clear() + if (modeEditNew === "new") { + modeEdit = "new" + timeStamp = ((new Date).getTime()).toString() + idTextfieldCurrencyProject.text = recentlyUsedCurrency + idTextfieldProjectname.text = "" + idTextfieldProjectname.forceActiveFocus() + } else { // edit project mode + modeEdit = "edit" + var tempProjectMembersArray = [] + tempProjectMembersArray = (listModel_allProjects.get(idComboboxProject.currentIndex).project_members).split(" ||| ") + for (var i = 0; i < tempProjectMembersArray.length ; i++) { + listModel_activeProjectMembersTEMP.append({ member_name : tempProjectMembersArray[i], + }) + } + timeStamp = (listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp).toString() + idTextfieldProjectname.text = listModel_allProjects.get(idComboboxProject.currentIndex).project_name + idTextfieldCurrencyProject.text = listModel_allProjects.get(idComboboxProject.currentIndex).project_base_currency + } + + // all members are beneficiaries + + + // show banner overlay + popup.opacity = 1.0 + } + + function hide() { + idTextfieldProjectname.focus = false + idTextfieldCurrencyProject.focus = false + + // clear all fields + idTextfieldProjectname.text = "" + idTextfieldCurrencyProject.text = recentlyUsedCurrency + + // make invisible + popup.opacity = 0.0 + } + + function addProjectDB () { + var project_id_timestamp = timeStamp + var project_name = idTextfieldProjectname.text + var project_members = "" + var project_recent_payer_boolarray = "" + var project_recent_beneficiaries_boolarray = "" + var project_base_currency = idTextfieldCurrencyProject.text + + for (var i = 0; i < listModel_activeProjectMembersTEMP.count ; i++) { + project_members += " ||| " + listModel_activeProjectMembersTEMP.get(i).member_name + project_recent_beneficiaries_boolarray += " ||| " + "true" + // recent_payer can only be one person, initially this will be the first entry of the list + if (i===0) { + project_recent_payer_boolarray += " ||| " + "true" + } else { + project_recent_payer_boolarray += " ||| " + "false" + } + } + // remove first occurance of " ||| " to later be able to split that string + project_members = project_members.replace(" ||| ", "") + project_recent_payer_boolarray = project_recent_payer_boolarray.replace(" ||| ", "") + project_recent_beneficiaries_boolarray = project_recent_beneficiaries_boolarray.replace(" ||| ", "") + + if (modeEdit === "new") { + // store in DB and list for new project + storageItem.setProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) + listModel_allProjects.append({ project_id_timestamp : Number(project_id_timestamp), + project_name : project_name, + project_members : project_members, + project_recent_payer_boolarray : project_recent_payer_boolarray, + project_recent_beneficiaries_boolarray : project_recent_beneficiaries_boolarray, + project_base_currency : project_base_currency, + }) + + // if this is the first project, auto set it as active project + if (listModel_allProjects.count === 1) { // auto-sets as currently active project, if this project is very first one + storageItem.setSettings("activeProjectID_unixtime", Number(project_id_timestamp) ) + activeProjectID_unixtime = Number(project_id_timestamp) + loadActiveProjectInfos_FromDB( Number(project_id_timestamp) ) + //console.log("auto set as active project ID = " + activeProjectID_unixtime) + } + + } else { // modeEdit === "edit + // update DB and list for existing project + storageItem.updateProject( project_id_timestamp, project_name, project_members, project_recent_payer_boolarray, project_recent_beneficiaries_boolarray, project_base_currency ) + for (var j = 0; j < listModel_allProjects.count ; j++) { + if (listModel_allProjects.get(j).project_id_timestamp === Number(project_id_timestamp)) { + //console.log("updated entry at: id_" + project_id_timestamp) + listModel_allProjects.set(j, { "project_name" : project_name, + "project_members" : project_members, + "project_recent_payer_boolarray" : project_recent_payer_boolarray, + "project_recent_beneficiaries_boolarray" : project_recent_beneficiaries_boolarray, + "project_base_currency" : project_base_currency + }) + } + } + } + updateEvenWhenCanceled = true + hide() + } + + function deleteProject() { + updateEvenWhenCanceled = true + storageItem.deleteProject(timeStamp) + listModel_allProjects.remove(tempProjectListIndex) + // set active project to reasonable one + if (idComboboxProject.currentIndex != 0) { + idComboboxProject.currentIndex = idComboboxProject.currentIndex -1 + } + //console.log("auto set after deleting ID = " + Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) // needed to make sure there is no expense or member list still active + hide() + } + + function clearProject() { + updateEvenWhenCanceled = true + listModel_activeProjectExpenses.clear() + storageItem.removeFullTable( "table_" + activeProjectID_unixtime.toString() ) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) // needed to make sure there is no expense or member list still active + hide() + } + +} + diff --git a/qml/pages/CalcPage.qml b/qml/pages/CalcPage.qml new file mode 100644 index 0000000..9ab11f8 --- /dev/null +++ b/qml/pages/CalcPage.qml @@ -0,0 +1,556 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Page { + id: pageResults + allowedOrientations: Orientation.All + onVisibleChanged: { + if (visible === true) { // entered page + listModel_activeProjectResults.clear() + listModel_activeProjectResultsSettlement.clear() + generateExchangeRateListFromExpenses() + addExchangeRates_listmodelActiveProjectExpenses() + } + } + + property real totalSpendingAmount_baseCurrency : 0 + property int counterShownExchangeRates : 0 + property string toShareString : "" + + ListModel { + id: listModel_activeProjectResultsSettlement + } + SilicaFlickable{ + id: listView + anchors.fill: parent + contentHeight: resultsColumn.height // tell overall height + + PullDownMenu { + MenuItem { + text: qsTr("Share detailed") + onClicked: { + createShareString("detailed") + } + } + MenuItem { + text: qsTr("Share compact") + onClicked: { + createShareString("compact") + } + } + } + + Column { + id: resultsColumn + x: Theme.paddingLarge + width: parent.width - 2*x + + Column { + width: parent.width + topPadding: Theme.paddingLarge + + Label { + width: parent.width + horizontalAlignment: Text.AlignRight + color: Theme.highlightColor + font.pixelSize: Theme.fontSizeLarge + text: qsTr("Results") + } + Label { + width: parent.width + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + text: activeProjectName + " [" + activeProjectCurrency + "]" + } + } + Item { + width: parent.width + height: Theme.paddingLarge + Theme.paddingSmall + } + Label { + visible: listModel_exchangeRates.count > 1 // main exchange rate is also counted in + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("EXCHANGE RATES") + } + Repeater { + id: idRepeaterExchangeRates + width: parent.width + model: listModel_exchangeRates + delegate: Column { + id: idColumnContent + visible: expense_currency !== activeProjectCurrency + width: parent.width + + Row { + width: parent.width + + Label { + width: parent.width / 3 + height: parent.height + topPadding: Theme.paddingSmall + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + text: (exchangeRateMode === 1) + ? (new Date(Number(date_time)).toLocaleString(Qt.locale(), "dd.MM.yy" + " - " + "hh:mm")) //"ddd dd.MM.yyyy - hh:mm" + : (qsTr("constant")) + } + Label { + width: parent.width / 3 + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + topPadding: Theme.paddingSmall + horizontalAlignment: Text.AlignRight + text: Number(1).toFixed(2) + " " + "" +expense_currency + "" + " = " + } + TextField { + id: idTextfieldExchangeRate + width: parent.width / 3 + textRightMargin: idLabelProjectCurrency.width + textLeftMargin: 0 + inputMethodHints: Qt.ImhFormattedNumbersOnly //use "Qt.ImhDigitsOnly" for INT + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: Number(exchange_rate) //.toFixed(decimalPlacesCurrencyRate) + EnterKey.onClicked: { + focus = false + } + onFocusChanged: { + if (text.length < 1) { + text = Number(exchange_rate) //.toFixed(decimalPlacesCurrencyRate) + } else { + text = text.replace(",", ".") + text = Number(text) //.toFixed(decimalPlacesCurrencyRate) + } + if (focus) { + selectAll() + } else { // unfocus + exchange_rate = Number(text) + storeExchangeRate_DB(expense_currency, exchange_rate) + addExchangeRates_listmodelActiveProjectExpenses() + } + } + Label { + id: idLabelProjectCurrency + anchors.left: parent.right + font.pixelSize: Theme.fontSizeSmall + //color: Theme.highlightColor + color: Theme.secondaryColor + text: "" + base_currency + "" + } + } + } + } + + } + Item { + width: parent.width + height: Theme.paddingLarge + } + + Label { + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("SPENDING OVERVIEW") + } + Row { + visible: listModel_activeProjectResults.count > 0 + width: parent.width + + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + font.pixelSize: Theme.fontSizeSmall + text: qsTr("name") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("payments") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("benefits") + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.italic: true + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: qsTr("saldo") + } + } + Repeater { + id: idRepeaterExpensesOverview + model: listModel_activeProjectResults + delegate: Row { + width: parent.width + + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + text: beneficiary_name + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: expense_sum.toFixed(2) + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: beneficiary_sum.toFixed(2) + } + Label { + width: parent.width / 4 + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + color: (( Number(expense_sum) - Number(beneficiary_sum) ).toFixed(2) < 0) ? Theme.errorColor : "green" + text: ( Number(expense_sum) - Number(beneficiary_sum) ).toFixed(2) + } + } + } + Row { + visible: listModel_activeProjectResults.count > 0 + width: parent.width + topPadding: Theme.paddingMedium + + Label { + width: parent.width / 4 * 2 + color: Theme.secondaryColor + font.bold: true + font.overline: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: totalSpendingAmount_baseCurrency.toFixed(2) + //text: activeProjectCurrency + " " + totalSpendingAmount_baseCurrency.toFixed(2) + } + Item { + width: parent.width / 4 + height: 1 + } + Label { + width: parent.width / 4 + color: Theme.secondaryColor + font.bold: true + font.overline: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: activeProjectCurrency + } + } + Item { + width: parent.width + height: Theme.paddingLarge * 2.5 + } + + Label { + width: parent.width + bottomPadding: Theme.paddingMedium + font.pixelSize: Theme.fontSizeSmall + color: Theme.secondaryColor + text: qsTr("SETTLEMENT SUGGESTION") + } + Repeater { + id: idRepeaterSettlementSuggestions + visible: model.count > 0 + model: listModel_activeProjectResultsSettlement + delegate: Row { + width: parent.width + + Label { + width: parent.width + font.pixelSize: Theme.fontSizeSmall + wrapMode: Text.WordWrap + text: (settling_sum.toFixed(2) >= 0) ? + ("" + from_name + " " + qsTr("owes") + " " + to_name + " " + qsTr("the sum of") + " " + settling_sum.toFixed(2) + " " + activeProjectCurrency + ".") + : ("" + to_name + " " + qsTr("owes") + " " + from_name + " " + qsTr("the sum of") + " " + (-settling_sum).toFixed(2) + " " + activeProjectCurrency + ".") + + } + } + } + Item { + width: parent.width + height: Theme.itemSizeSmall + } + } + } + + + function generateExchangeRateListFromExpenses() { + listModel_exchangeRates.clear() + + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + if (exchangeRateMode === 0 ) { // collective currency + // check if available and where occuring + var currencyAlreadySet = false + var addCurrencyIndex = 0 + for (var j = 0; j < listModel_exchangeRates.count; j++) { + if ( listModel_activeProjectExpenses.get(i).expense_currency === listModel_exchangeRates.get(j).expense_currency ) { + currencyAlreadySet = true + addCurrencyIndex = j + } + } + // if it has not been set yet, add to exchange rate list + if (currencyAlreadySet === false) { + var tempStoredExchangeRateDB = Number(storageItem.getExchangeRate(listModel_activeProjectExpenses.get(i).expense_currency, 1)) + listModel_exchangeRates.append({ expense_currency : listModel_activeProjectExpenses.get(i).expense_currency, + base_currency : activeProjectCurrency, + exchange_rate : tempStoredExchangeRateDB, + date_time : Number(0) + }) + } + } else { // exchangeRateMode === 1 ... individual transactions + var tempExpenseCurrency = listModel_activeProjectExpenses.get(i).expense_currency + if (tempExpenseCurrency !== activeProjectCurrency) { + tempStoredExchangeRateDB = Number(storageItem.getExchangeRate(listModel_activeProjectExpenses.get(i).expense_currency, 1)) + listModel_exchangeRates.append({ expense_currency : listModel_activeProjectExpenses.get(i).expense_currency, + base_currency : activeProjectCurrency, + exchange_rate : tempStoredExchangeRateDB, + date_time : Number(listModel_activeProjectExpenses.get(i).date_time) + }) + } + } + } + } + + function storeExchangeRate_DB (exchange_rate_currency, exchange_rate_value) { + var tempOccurences = Number(storageItem.countExchangeRateOccurances(exchange_rate_currency, 0)) + if (tempOccurences === 0) { // add new entry + storageItem.setExchangeRate(exchange_rate_currency, exchange_rate_value) + } else { // update existing entry + storageItem.updateExchangeRate(exchange_rate_currency, exchange_rate_value) + } + } + + function addExchangeRates_listmodelActiveProjectExpenses() { + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempExpenseCurrency = listModel_activeProjectExpenses.get(i).expense_currency + var tempExpenseDateTime = Number(listModel_activeProjectExpenses.get(i).date_time) + + if (tempExpenseCurrency === activeProjectCurrency) { // paid in project specific currency + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(1)}) + } else { // if paid in different currency + + if ( exchangeRateMode === 0 ) { // ... collective currency + for (var k = 0; k < listModel_exchangeRates.count; k++) { + if (listModel_exchangeRates.get(k).expense_currency === tempExpenseCurrency) { + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(listModel_exchangeRates.get(k).exchange_rate)}) + //console.log(Number(listModel_exchangeRates.get(k).exchange_rate)) + } + } + } else { // ... individual transactions + for (k = 0; k < listModel_exchangeRates.count; k++) { + if ((listModel_exchangeRates.get(k).expense_currency === tempExpenseCurrency) && (Number(listModel_exchangeRates.get(k).date_time) === tempExpenseDateTime)) { + listModel_activeProjectExpenses.set(i, {"exchange_rate": Number(listModel_exchangeRates.get(k).exchange_rate)}) + } + } + } + } + } + calculateResults_members() + } + + function calculateResults_members () { + listModel_activeProjectResults.clear() + listModel_activeProjectResultsSettlement.clear() + totalSpendingAmount_baseCurrency = 0 + + // sum up benefits per member + for (var i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempBeneficiariesArray = (listModel_activeProjectExpenses.get(i).expense_members).split(" ||| ") + for (var j = 0; j < tempBeneficiariesArray.length; j++) { + var tempExpense_inBaseCurrency_perBeneficiariy = ((Number(listModel_activeProjectExpenses.get(i).expense_sum) / tempBeneficiariesArray.length) * Number(listModel_activeProjectExpenses.get(i).exchange_rate)) + + // cycle through results list, check if beneficiary is already available and where + var tempBeneficiaryAvailable = false + var tempBeneficiaryIndex = 0 + for (var k = 0; k < listModel_activeProjectResults.count; k++ ) { + if (listModel_activeProjectResults.get(k).beneficiary_name === tempBeneficiariesArray[j]) { + tempBeneficiaryAvailable = true + tempBeneficiaryIndex = k + } + } + // if not available, add him to results list (only on first occurance) + if (tempBeneficiaryAvailable === false) { + //console.log("first entry for: " + tempBeneficiariesArray[j]) + //console.log(tempExpense_inBaseCurrency_perBeneficiariy + " " + activeProjectCurrency) + listModel_activeProjectResults.append({ beneficiary_name : tempBeneficiariesArray[j], + beneficiary_sum : Number(tempExpense_inBaseCurrency_perBeneficiariy), + base_currency : activeProjectCurrency, + expense_sum : 0 // this info gets added later + }) + } else { // otherwise add his share to the existing list + var tempBeneficiarySum = Number(listModel_activeProjectResults.get(tempBeneficiaryIndex).beneficiary_sum) + Number(tempExpense_inBaseCurrency_perBeneficiariy) + listModel_activeProjectResults.set(tempBeneficiaryIndex, { "beneficiary_sum": Number(tempBeneficiarySum) }) + //console.log("latest benefitSum for " + tempBeneficiariesArray[j] + " is: " + tempBeneficiarySum) + } + //console.log(tempBeneficiariesArray[j]) + //console.log(tempExpense_inBaseCurrency_perBeneficiariy) + } + } + + // once listModel_activeProjectResults is created, go over expenses again and add payments + for ( i = 0; i < listModel_activeProjectExpenses.count; i++) { + var tempExpensePayer = listModel_activeProjectExpenses.get(i).expense_payer + var tempExpense_inBaseCurrency = Number(listModel_activeProjectExpenses.get(i).expense_sum) * Number(listModel_activeProjectExpenses.get(i).exchange_rate) + totalSpendingAmount_baseCurrency += tempExpense_inBaseCurrency + //console.log(tempExpensePayer) + //console.log(tempExpense_inBaseCurrency) + + // check if payer is already in results list as benefitter + var tempPayerAvailable = false + var tempPayerIndex = 0 + for (var l = 0; l < listModel_activeProjectResults.count; l++ ) { + if (listModel_activeProjectResults.get(l).beneficiary_name === tempExpensePayer) { + tempPayerAvailable = true + tempPayerIndex = l + } + } + + // if not available, add him to results list (only on first occurance) + if (tempPayerAvailable === false) { + //console.log("first entry for payer: " + tempExpensePayer + " = " + tempExpense_inBaseCurrency + " " + activeProjectCurrency) + //console.log("seems he paid but never benefits from anything") + listModel_activeProjectResults.append({ beneficiary_name : tempExpensePayer, + beneficiary_sum : 0, + base_currency : activeProjectCurrency, + expense_sum : Number(tempExpense_inBaseCurrency), + }) + } else { // otherwise add his share to the existing list + var tempPayerSum = Number(listModel_activeProjectResults.get(tempPayerIndex).expense_sum) + Number(tempExpense_inBaseCurrency) + listModel_activeProjectResults.set(tempPayerIndex, { "expense_sum": Number(tempPayerSum) }) + //console.log("latest payerSum for " + tempExpensePayer + " is: " + tempPayerSum) + } + } + + // sort results list according + listModel_activeProjectResults.quick_sort("desc") // payer with highest expense on top + + + // apply a (n-1) algorithm to settle expenses (how much each person ows to whom) + var outstandingNamesArray = [] + var outstandingSumsArray = [] + var totalSum = 0 + // iter backwards to get smallest amounts first, since listModel_activeProjectResults starts with highest expenses + for (var m = listModel_activeProjectResults.count-1; m >= 0; m--) { + outstandingNamesArray.push(listModel_activeProjectResults.get(m).beneficiary_name) + outstandingSumsArray.push( Number(listModel_activeProjectResults.get(m).expense_sum) - Number(listModel_activeProjectResults.get(m).beneficiary_sum) ) + totalSum += ( Number(listModel_activeProjectResults.get(m).expense_sum) - Number(listModel_activeProjectResults.get(m).beneficiary_sum) ) + } + //console.log(outstandingNamesArray) + //console.log(outstandingSumsArray) + //console.log(totalSum) + + function splitPayments() { + const mean = totalSum / outstandingNamesArray.length + var sortedValuesPaid = [] + for (var n = 0; n < outstandingSumsArray.length; n++) { + sortedValuesPaid.push(outstandingSumsArray[n] - mean) + } + var i = 0 + var j = outstandingNamesArray.length - 1 + var debt + while (i < j) { + debt = Math.min(-(sortedValuesPaid[i]), sortedValuesPaid[j]) + sortedValuesPaid[i] += debt + sortedValuesPaid[j] -= debt + listModel_activeProjectResultsSettlement.append({ from_name : outstandingNamesArray[i], + to_name : outstandingNamesArray[j], + settling_sum : Number(debt), + }) + //console.log(outstandingNamesArray[i] + " owes " + outstandingNamesArray[j] + " the sum of " + debt ) + if (sortedValuesPaid[i] === 0) { i++ } + if (sortedValuesPaid[j] === 0) { j-- } + } + } + splitPayments() + } + + function createShareString (detailGrade) { + + // create a shareable string + toShareString = "\n" + qsTr("Project:") + " " + activeProjectName + + "\n" + qsTr("Total expenses") + " = " + + totalSpendingAmount_baseCurrency.toFixed(2) + " " + activeProjectCurrency + + "\n" + "\n" + for (var i = 0; i < listModel_activeProjectResults.count; i++) { + toShareString += listModel_activeProjectResults.get(i).beneficiary_name + ":" + + "\n" + qsTr("payed") + " " + listModel_activeProjectResults.get(i).expense_sum.toFixed(2) + " " + activeProjectCurrency + + "\n" + qsTr("received") + " " + listModel_activeProjectResults.get(i).beneficiary_sum.toFixed(2) + " " + activeProjectCurrency + + "\n" + qsTr("saldo") + " " + (Number(listModel_activeProjectResults.get(i).expense_sum) - Number(listModel_activeProjectResults.get(i).beneficiary_sum)).toFixed(2) + " " + activeProjectCurrency + + "\n" + "\n" + } + toShareString += qsTr("Settlement suggestion:") + + "\n" + for (i = 0; i < listModel_activeProjectResultsSettlement.count; i++) { + if ( Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2) >= 0 ) { + toShareString += listModel_activeProjectResultsSettlement.get(i).from_name + + " " + qsTr("owes") + " " + + listModel_activeProjectResultsSettlement.get(i).to_name + + " " + qsTr("the sum of") + " " + Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2) + " " + activeProjectCurrency + + "." + "\n" + } else { // amount < 0 + toShareString += listModel_activeProjectResultsSettlement.get(i).to_name + + " " + qsTr("owes") + " " + + listModel_activeProjectResultsSettlement.get(i).from_name + + " " + qsTr("the sum of") + " " + (-1*Number(listModel_activeProjectResultsSettlement.get(i).settling_sum).toFixed(2)) + " " + activeProjectCurrency + + "." + "\n" + } + } + //console.log(toShareString) + + + // add details if necessary + if (detailGrade === "detailed") { + toShareString += "\n" + "\n" + "\n" + + qsTr("Detailed Spendings:") + + "\n" + "\n" + for ( i = 0; i < listModel_activeProjectExpenses.count; i++) { + if (sortOrderExpenses === 0) { // 0=descending, 1=ascending + var tmpEntryNumber = (listModel_activeProjectExpenses.count - i) + } else { + tmpEntryNumber = (i+1) + } + toShareString += qsTr("Expense #") + tmpEntryNumber + + "\n" + qsTr("date:") + " " + new Date(Number(listModel_activeProjectExpenses.get(i).date_time)).toLocaleString(Qt.locale(), "ddd dd.MM.yyyy - hh:mm") + + "\n" + qsTr("payer:") + " " + listModel_activeProjectExpenses.get(i).expense_payer + + "\n" + qsTr("item:") + " " + listModel_activeProjectExpenses.get(i).expense_name + + "\n" + qsTr("price:") + " " + Number(listModel_activeProjectExpenses.get(i).expense_sum).toFixed(2) + " " + listModel_activeProjectExpenses.get(i).expense_currency + + "\n" + qsTr("beneficiaries:") + " " + listModel_activeProjectExpenses.get(i).expense_members.split(" ||| ").join(", ") // .replace(" ||| ", ", ") + + "\n" + qsTr("info:") + " " + listModel_activeProjectExpenses.get(i).expense_info + + "\n" + "\n" + } + } + console.log(toShareString) + + // send this string + Clipboard.text = toShareString + shareActionText.mimeType = "text/plain" // "text/*" or "application/text" + shareActionText.resources = [{ + "data": toShareString, + "name": activeProjectName, + } ] + shareActionText.trigger() + } + +} diff --git a/qml/pages/FirstPage.qml b/qml/pages/FirstPage.qml new file mode 100644 index 0000000..5b6c63a --- /dev/null +++ b/qml/pages/FirstPage.qml @@ -0,0 +1,445 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Share 1.0 // ToDo: instead of copy to clipboard, send to app + +Page { + id: page + allowedOrientations: Orientation.All + + + // project specific global variables, loaded when activating a new project + property double activeProjectID_unixtime : Number(storageItem.getSettings("activeProjectID_unixtime", 0)) + property int activeProjectID_listIndex + property string activeProjectName + property string activeProjectCurrency : "EUR" + + + // program specific global variables + property int sortOrderExpenses : Number(storageItem.getSettings("sortOrderExpensesIndex", 0)) // 0=descending, 1=ascending + property int exchangeRateMode : Number(storageItem.getSettings("exchangeRateModeIndex", 0)) // 0=collective, 1=individual + property int interativeScrollbarMode : Number(storageItem.getSettings("interativeScrollbarMode", 0)) // 0=standard, 1=interactive + property string recentlyUsedCurrency : storageItem.getSettings("recentlyUsedCurrency", activeProjectCurrency) + + // navigation specific blocking + property bool updateEvenWhenCanceled : false + property bool delegateMenuOpen : false + + + // autostart + Component.onCompleted: { + generateAllProjectsList_FromDB() + loadActiveProjectInfos_FromDB(activeProjectID_unixtime) + } + + + + // other items, components and pages + ListModel { + id: listModel_allProjects + } + ListModel { + id: listModel_activeProjectMembers + } + ListModel { + id: listModel_activeProjectExpenses + property string sortColumnName: "date_time" //"id_unixtime_created" + function swap(a,b) { + if (ab) { + move(b,a,1); + move (a-1,b,1); + } + } + function partition(begin, end, pivot) { + var piv=get(pivot)[sortColumnName]; + swap(pivot, end-1); + var store=begin; + var ix; + for(ix=begin; ix piv) { + swap(store,ix); + ++store; + } + } + } + swap(end-1, store); + return store; + } + function qsort(begin, end) { + if(end-1>begin) { + var pivot=begin+Math.floor(Math.random()*(end-begin)); + + pivot=partition( begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot+1, end); + } + } + function quick_sort() { + qsort(0,count) + } + + onCountChanged: { + quick_sort() + } + } + ListModel { + id: listModel_exchangeRates + } + ListModel { + id: listModel_activeProjectResults + property string sortColumnName : "expense_sum" + property string sortOrderResults : "desc" //"asc" + function swap(a,b) { + if (ab) { + move(b,a,1); + move (a-1,b,1); + } + } + function partition(begin, end, pivot) { + var piv=get(pivot)[sortColumnName]; + swap(pivot, end-1); + var store=begin; + var ix; + for(ix=begin; ix piv) { + swap(store,ix); + ++store; + } + } + } + swap(end-1, store); + return store; + } + function qsort(begin, end) { + if(end-1>begin) { + var pivot=begin+Math.floor(Math.random()*(end-begin)); + + pivot=partition( begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot+1, end); + } + } + function quick_sort(orderDirection) { + sortOrderResults = orderDirection + qsort(0,count) + } + } + SettingsPage { + id: settingsPage + } + CalcPage { + id: calcPage + } + BannerAddExpense { + id: bannerAddExpense + } + Component { + id: datePickerComponent + DatePickerDialog {} + } + Component { + id: timePickerComponent + TimePickerDialog {} + } + ShareAction { + id: shareActionText + } + + + + // main page, current project + SilicaListView { + id: idSilicaListView + anchors.fill: parent + header: Row { + width: (interativeScrollbarMode === 0) ? (parent.width) : ((isPortrait) ? (parent.width) : (parent.width - Theme.paddingLarge*2)) + visible: activeProjectID_unixtime !== 0 + topPadding: Theme.paddingLarge + bottomPadding: Theme.paddingLarge + + + Item { + id: idLeftSpacer + width: Theme.paddingSmall + Theme.paddingMedium + height: 1 + } + Column { + id: idHeaderInfoColumn + width: parent.width - idLeftSpacer.width + bottomPadding: Theme.paddingSmall + + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeLarge + color: Theme.highlightColor + wrapMode: Text.WordWrap + text: qsTr("Expenses") + } + Label { + x: Theme.paddingMedium + width: parent.width - 2*x + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + color: Theme.highlightColor + wrapMode: Text.WordWrap + //text: "ID_" + activeProjectID_unixtime + " [" + activeProjectCurrency + "]" + text: activeProjectName + " [" + activeProjectCurrency + "]" + } + } + } + footer: Item { + width: parent.width + height: Theme.itemSizeSmall + } + spacing: Theme.paddingMedium + quickScroll: (interativeScrollbarMode === 0) ? (true) : (false) + + VerticalScrollDecorator { + enabled: (interativeScrollbarMode === 0) ? (true) : (false) + visible: enabled + } + ScrollBar { + id: idScrollBarDate + enabled: (interativeScrollbarMode === 0) ? false : true + labelVisible: true + topPadding: (isPortrait) ? (Theme.itemSizeLarge + Theme.paddingLarge) : (0) + bottomPadding: (isPortrait) ? Theme.itemSizeSmall : 0 + labelModelTag: "date_time" + visible: (interativeScrollbarMode === 0) ? (false) : (idPulldownMenu.active === false) && (delegateMenuOpen === false) + } + PullDownMenu { + id: idPulldownMenu + quickSelect: true + + MenuItem { + text: qsTr("Settings") + onClicked: pageStack.push(settingsPage) + } + MenuItem { + text: qsTr("Calculate") + enabled: activeProjectID_unixtime !== 0 + onClicked: pageStack.animatorPush(calcPage) + } + MenuItem { + text: qsTr("Add") + enabled: activeProjectID_unixtime !== 0 + onClicked: bannerAddExpense.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", activeProjectID_unixtime, 0 ) + } + } + ViewPlaceholder { + enabled: activeProjectID_unixtime === 0 // listModel_allProjects.count === 0 + text: qsTr("Create new project.") + hintText: qsTr("Nothing loaded yet.") + } + + model: listModel_activeProjectExpenses + delegate: ListItem { + id: idListItem + contentHeight: idListLabelsQML.height + contentWidth: (interativeScrollbarMode === 0) ? (parent.width) : (parent.width - idScrollBarDate.width) + //contentWidth: (idSilicaListView.visibleArea.heightRatio < 1.0 && idPulldownMenu.active === false) ? (parent.width - idScrollBarDate.width) : (parent.width) + menu: ContextMenu { + id: idContextMenu + + MenuItem { + text: qsTr("Edit") + onClicked: { + bannerAddExpense.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "edit", activeProjectID_unixtime, id_unixtime_created ) + } + } + MenuItem { + text: qsTr("Remove") + onClicked: { + idRemorseDelete.execute(idListItem, qsTr("Remove entry?"), function() { + storageItem.deleteExpense(activeProjectID_unixtime, id_unixtime_created ) + listModel_activeProjectExpenses.remove(index) + } ) + } + } + } + onMenuOpenChanged: { + // set variable to disable scrollBar visibility + if (menuOpen === true) { + delegateMenuOpen = true + } else { + delegateMenuOpen = false + } + } + + RemorseItem { + id: idRemorseDelete + } + Row { + width: parent.width + + Rectangle { + width: Theme.paddingSmall + height: idListLabelsQML.height + color: Theme.rgba(Theme.highlightBackgroundColor, 0.4) + } + Item { + width: Theme.paddingMedium + height: 1 + } + Column { + id: idListLabelsQML + width: parent.width - Theme.paddingSmall - 2*Theme.paddingMedium + + Row { + width: parent.width + + Label { + width: parent.width/3*2- Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + text: new Date(Number(date_time)).toLocaleString(Qt.locale(), "ddd dd.MM.yyyy - hh:mm") + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width/3 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + horizontalAlignment: Text.AlignRight + text: expense_payer + } + } + Row { + width: parent.width + + Label { + width: parent.width/3*2 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeSmall + text: expense_name + } + Item { + width: Theme.paddingLarge + height: 1 + } + Label { + width: parent.width/3 - Theme.paddingLarge/2 + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + font.pixelSize: Theme.fontSizeSmall + text: Number(expense_sum).toFixed(2) + " " + expense_currency.toString() + } + } + Label { + id: idLabelExpenseInfo + visible: idLabelExpenseInfo.text.length > 0 + width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: expense_info + } + Label { + id: idLabelBeneficiaries + width: parent.width + wrapMode: Text.WordWrap + font.pixelSize: Theme.fontSizeTiny + color: Theme.secondaryColor + text: qsTr("group:") + " " + expense_members.split(" ||| ").join(", ") + } + Item { + width: parent.width + height: Theme.paddingSmall + } + } + } + } + } // end SilicaListView + + + + + function generateAllProjectsList_FromDB() { + listModel_allProjects.clear() + var allProjectsOverview = storageItem.getAllProjects("none") + //console.log(allProjectsOverview) + if (allProjectsOverview !== "none") { + for (var i = 0; i < allProjectsOverview.length ; i++) { + + listModel_allProjects.append({ + project_id_timestamp : Number(allProjectsOverview[i][0]), + project_name : allProjectsOverview[i][1], + project_members : allProjectsOverview[i][2], + project_recent_payer_boolarray : allProjectsOverview[i][3], + project_recent_beneficiaries_boolarray : allProjectsOverview[i][4], + project_base_currency : allProjectsOverview[i][5], + }) + } + } + } + function loadActiveProjectInfos_FromDB(activeProjectID_unixtime) { + //console.log( "loading project: " + Number(activeProjectID_unixtime) ) + listModel_activeProjectMembers.clear() + listModel_activeProjectExpenses.clear() + for (var j = 0; j < listModel_allProjects.count ; j++) { + //console.log("in listmodel: " + Number(listModel_allProjects.get(j).project_id_timestamp) ) + // only use active project infos + if ( Number(listModel_allProjects.get(j).project_id_timestamp) === Number(activeProjectID_unixtime) ) { + // find active project name and currency + activeProjectName = listModel_allProjects.get(j).project_name + activeProjectID_listIndex = j + activeProjectCurrency = listModel_allProjects.get(j).project_base_currency + + // generate active project members list + var activeProjectMembersArray = (listModel_allProjects.get(j).project_members).split(" ||| ") + var activeProjectRecentPayerBoolArray = (listModel_allProjects.get(j).project_recent_payer_boolarray).split(" ||| ") + var activeProjectRecentBeneficiariesBoolArray = (listModel_allProjects.get(j).project_recent_beneficiaries_boolarray).split(" ||| ") + for (var i = 0; i < activeProjectMembersArray.length ; i++) { + listModel_activeProjectMembers.append({ + member_name : activeProjectMembersArray[i], + member_isBeneficiary : activeProjectRecentBeneficiariesBoolArray[i], + member_isPayer : activeProjectRecentPayerBoolArray[i], + }) + } + + // generate active project expenses list + var currentProjectEntries = storageItem.getAllExpenses( activeProjectID_unixtime, "none") + if (currentProjectEntries !== "none") { + for (i = 0; i < currentProjectEntries.length ; i++) { + listModel_activeProjectExpenses.append({ + id_unixtime_created : Number(currentProjectEntries[i][0]).toFixed(0), + date_time : Number(currentProjectEntries[i][1]).toFixed(0), + expense_name : currentProjectEntries[i][2], + expense_sum : Number(currentProjectEntries[i][3]).toFixed(2), + expense_currency : currentProjectEntries[i][4], + expense_info : currentProjectEntries[i][5], + expense_payer : currentProjectEntries[i][6], + expense_members : currentProjectEntries[i][7], + }) + } + } + } + } + } + +} diff --git a/qml/pages/ScrollBar.qml b/qml/pages/ScrollBar.qml new file mode 100755 index 0000000..7645a89 --- /dev/null +++ b/qml/pages/ScrollBar.qml @@ -0,0 +1,106 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + + +Rectangle { + // inherited values + property var flickable: parent + property bool labelVisible : true + property string labelModelTag : "" + property real topPadding : 0 + property real bottomPadding : 0 + property color handleColor : Theme.highlightBackgroundColor + + // own values but overwritable + property int scrollToNumber : 0 + property string showLabel : "" + property int listcountMax : flickable.count // (flickable.count !== undefined) ? flickable.count : 0 // dirty bugfix for scrollBar wron position on empty lists + property real roundCornersRadius : Theme.paddingLarge + + id: scrollbar + anchors.top: parent.top + anchors.topMargin: topPadding + anchors.bottom: parent.bottom + anchors.bottomMargin: bottomPadding + anchors.right: parent.right + width: roundCornersRadius * 2 + radius: width / 2 + color: Theme.rgba(Theme.primaryColor, 0.075) + visible: flickable.visibleArea.heightRatio < 1.0 + + Rectangle { + id: handle + width: parent.width + height: Math.max(roundCornersRadius*2, flickable.visibleArea.heightRatio * scrollbar.height) + color: handleColor + opacity: (clicker.drag.active ) ? 1 : 0.4 + radius: width / 2 + } + Rectangle { + id: scrollLabelBackground + visible: labelVisible + anchors.verticalCenter: handle.verticalCenter + anchors.right: handle.left + anchors.rightMargin: Theme.paddingLarge * 1.5 + width: scrollLabel.width + height*1.2 + height: roundCornersRadius * 2 + radius: roundCornersRadius + color: handleColor + opacity: (clicker.drag.active ) ? 1 : 0 + + Label { + id: scrollLabel + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + //text: showLabel + text: new Date(Number(showLabel)).toLocaleString(Qt.locale(), "dd.MM.yyyy - hh:mm") + } + Rectangle { + anchors.left: parent.right + anchors.leftMargin: -roundCornersRadius + anchors.verticalCenter: parent.verticalCenter + width: parent.anchors.rightMargin + 2*roundCornersRadius + height: Theme.paddingSmall/3*2 + color: parent.color + } + } + Binding { + // jump handle to where the currently visible part of flickable ist + target: handle + property: "y" + value: (flickable.visibleArea.yPosition) * (scrollbar.height - ( handle.height - (flickable.visibleArea.heightRatio * scrollbar.height))) + when: !clicker.pressed + } + MouseArea { + id: clicker + anchors.fill: parent + anchors.rightMargin: -Theme.paddingSmall + anchors.leftMargin: -Theme.paddingSmall + preventStealing: true + drag { + target: handle + minimumY: 0 + maximumY: scrollbar.height - handle.height + axis: Drag.YAxis + } + onMouseYChanged: { + //flickable.contentY = handle.y / drag.maximumY * (flickable.contentHeight - flickable.height) + scrollToNumber = Math.ceil(handle.y / drag.maximumY * listcountMax) + if (scrollToNumber < 0) { + scrollToNumber = 0 + } else if (scrollToNumber >= listcountMax) { + scrollToNumber = listcountMax -1 // because index starts at 0, while count() starts at 1 + } + showLabel = (flickable.model.get(scrollToNumber)[labelModelTag] !== undefined) ? flickable.model.get(scrollToNumber)[labelModelTag] : "" //flickable.model.get(scrollToNumber)[labelModelTag] + flickable.positionViewAtIndex( scrollToNumber, ListView.Center) + //flickable.contentY = handle.y / drag.maximumY * (flickable.contentHeight - flickable.height) + } + onClicked: { + //flickable.contentY = mouse.y / scrollbar.height * (flickable.contentHeight - flickable.height) + scrollToNumber = Math.ceil(mouse.y / scrollbar.height * listcountMax) + showLabel = (flickable.model.get(scrollToNumber)[labelModelTag] !== undefined) ? flickable.model.get(scrollToNumber)[labelModelTag] : "" //flickable.model.get(scrollToNumber)[labelModelTag] + flickable.positionViewAtIndex( scrollToNumber, ListView.Center ) + } + //onReleased: console.log("released") + } +} diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml new file mode 100644 index 0000000..c655f0a --- /dev/null +++ b/qml/pages/SettingsPage.qml @@ -0,0 +1,456 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 +import Sailfish.Pickers 1.0 +import FileIO 1.0 +import Nemo.Notifications 1.0 + + + +Dialog { + id: pageSettings + + property int showMaintenanceButtonsCounter : 0 + property string notificationString : "" + //property int oldProjectIndex + + allowedOrientations: Orientation.Portrait + backNavigation: (bannerAddProject.opacity < 1) // && (updateEvenWhenCanceled === false) + forwardNavigation: (bannerAddProject.opacity < 1) + onOpened: { + //console.log("old project index = " + activeProjectID_unixtime) + showMaintenanceButtonsCounter = 0 + updateEvenWhenCanceled = false + idComboboxProject.currentIndex = 0 + for (var j = 0; j < listModel_allProjects.count ; j++) { + if (Number(listModel_allProjects.get(j).project_id_timestamp) === Number(storageItem.getSettings("activeProjectID_unixtime", 0))) { + idComboboxProject.currentIndex = j + } + } + idComboboxSortingExpenses.currentIndex = Number(storageItem.getSettings("sortOrderExpensesIndex", 0)) // 0=descending, 1=ascending + idComboboxExchangeRateMode.currentIndex = Number(storageItem.getSettings("exchangeRateModeIndex", 0)) // 0=collective, 1=individual + idComboboxShowInteractiveScrollbar.currentIndex = Number(storageItem.getSettings("interativeScrollbarMode", 0)) //0=standard, 1=interactive + notificationString = "" + } + onDone: { + if (result == DialogResult.Accepted) { + writeDB_Settings() + } + } + onRejected: { + // in certain cases reload list even on cancel: if project was cleared, delted or created + // then use previous activeProjectID_unixtime instead of the new one from dropdown menu + if (updateEvenWhenCanceled === true) { + loadActiveProjectInfos_FromDB(activeProjectID_unixtime) + updateEvenWhenCanceled = false + } + } + + BannerAddProject { + id: bannerAddProject + } + Banner2ButtonsChoice { + id: banner2ButtonsChoice + } + Notification { + id: notificationBackup + expireTimeout: 4000 + //appName: qsTr("Expenditure") + //icon: "image://theme/icon-lock-warning" + + function showSmall(message) { + replacesId = 0 + previewSummary = "" + previewBody = message + publish() + } + function showBig(title, message) { + replacesId = 0 + previewSummary = title + previewBody = message + publish() + } + } + RemorsePopup { + z: 10 + id: remorse_clearExchangeRatesDB + } + RemorsePopup { + z: 10 + id: remorse_restoreBackupFile + } + ListModel { + id: listModel_tempProjectExpenses + } + Component { + id: idFolderPickerPage + + FolderPickerPage { + dialogTitle: qsTr("Backup to") + onSelectedPathChanged: { + backupProjectExpenses( selectedPath ) + } + } + } + Component { + id: idFilePickerPage + + FilePickerPage { + title: qsTr("Restore backup file") + nameFilters: [ '*.csv' ] + onSelectedContentPropertiesChanged: { + var selectedPath = selectedContentProperties.filePath + idTextFileBackup.source = selectedPath + var tempProjectIndex_File = ((idTextFileBackup.text).split("\n"))[0] + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + var headlineText = qsTr("Restore backup - choose action") + var otherText = qsTr("Replace deletes all former project-expenses and uses those from backup-file instead.") + " " + + qsTr("Merge keeps former project-expenses and adds those from backup-file which are not yet on the list.") + if (parseInt(tempProjectIndex) !== parseInt(tempProjectIndex_File)) { + var detailText = qsTr("File info: This backup was created by a different project.") + } else { + detailText = qsTr("File info: This backup was created by the original project.") + } + var choiceText_1 = qsTr("Replace") + var choiceText_2 = qsTr("Merge") + banner2ButtonsChoice.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, headlineText, detailText, otherText, choiceText_1, choiceText_2, selectedPath ) + } + } + } + TextFileIO { + id: idTextFileBackup + } + + + + SilicaFlickable{ + anchors.fill: parent + contentHeight: column.height // tell overall height + + Column { + id: column + width: pageSettings.width + + DialogHeader { + //title: qsTr("Settings") + } + Row { + width: parent.width + bottomPadding: Theme.paddingLarge + + Label { + id: idLabelSettingsHeader + width: parent.width / 6 * 5 + leftPadding: Theme.paddingLarge + font.pixelSize: Theme.fontSizeExtraLarge + color: Theme.highlightColor + text: qsTr("Settings") + + MouseArea { + anchors.fill: parent + onClicked: showMaintenanceButtonsCounter = showMaintenanceButtonsCounter + 1 + } + } + IconButton { + width: parent.width / 6 + anchors.verticalCenter: idLabelSettingsHeader.verticalCenter + icon.color: Theme.highlightColor + icon.scale: 1.1 + icon.source: "image://theme/icon-m-about?" + onClicked: { + pageStack.animatorPush(Qt.resolvedUrl("AboutPage.qml"), {}) + } + } + } + Row { + width: parent.width + + ComboBox { + id: idComboboxProject + width: (listModel_allProjects.count === 0) ? (parent.width / 6*5) : (parent.width / 6*4) + label: qsTr("Project") + menu: ContextMenu { + + Repeater { + enabled: listModel_allProjects.count > 0 + model: listModel_allProjects + + MenuItem { + text: project_name + } + } + } + + MouseArea { + enabled: listModel_allProjects.count === 0 + anchors.fill: parent + preventStealing: true + onClicked: bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", idComboboxProject.currentIndex ) + } + } + IconButton { + visible: listModel_allProjects.count > 0 + width: parent.width / 6 + icon.source: "image://theme/icon-m-edit?" + onClicked: { + bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "edit", idComboboxProject.currentIndex ) + } + } + IconButton { + width: parent.width / 6 + icon.source: "image://theme/icon-m-add?" + onClicked: { + bannerAddProject.notify( Theme.rgba(Theme.highlightDimmerColor, 1), Theme.itemSizeLarge, "new", idComboboxProject.currentIndex ) + } + } + } + ComboBox { + id: idComboboxSortingExpenses + width: parent.width + label: qsTr("Sorting") + + menu: ContextMenu { + MenuItem { + text: qsTr("descending") + } + MenuItem { + text: qsTr("ascending") + } + } + } + ComboBox { + id: idComboboxExchangeRateMode + width: parent.width + label: qsTr("Exchange rate") + menu: ContextMenu { + MenuItem { + text: qsTr("per currency (constant)") + } + MenuItem { + text: qsTr("per transaction (dates)") + } + } + } + ComboBox { + id: idComboboxShowInteractiveScrollbar + width: parent.width + label: qsTr("Scrollbar") + menu: ContextMenu { + MenuItem { + text: qsTr("normal") + } + MenuItem { + text: qsTr("interactive (beta)") + } + } + } + Column { + visible: showMaintenanceButtonsCounter > 9 + width: parent.width + topPadding: Theme.paddingLarge * 3 + spacing: Theme.paddingLarge + + Label { + width: parent.width + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + color: Theme.errorColor + text: qsTr("Database cleanup - requires restart:") + leftPadding: Theme.paddingLarge + Theme.paddingSmall + rightPadding: leftPadding + } + Button { + text: qsTr("settings") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored settings?"), function() { + storageItem.removeFullTable( "settings_table" ) + }) + } + } + Button { + text: qsTr("exchange rates") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored exchange rates?"), function() { + storageItem.removeFullTable( "exchange_rates_table" ) + }) + } + } + Button { + text: qsTr("projects") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + remorse_clearExchangeRatesDB.execute(qsTr("Delete stored projects?"), function() { + // remove all individual project expense tables + for (var j = 0; j < listModel_allProjects.count ; j++) { + var tempTablename = "table_" + listModel_allProjects.get(j).project_id_timestamp + storageItem.removeFullTable(tempTablename) + } + // remove all projects overview table + storageItem.removeFullTable( "projects_table" ) + // set latest setting_id to zero + activeProjectID_unixtime = 0 + storageItem.setSettings("activeProjectID_unixtime", activeProjectID_unixtime) + // update front page + updateEvenWhenCanceled = true + }) + } + } + } + Item { + width: parent.width + height: Theme.paddingLarge + } + } + } + + + + // ******************************************** important functions ******************************************** // + + function writeDB_Settings() { + storageItem.setSettings("sortOrderExpensesIndex", idComboboxSortingExpenses.currentIndex) + sortOrderExpenses = idComboboxSortingExpenses.currentIndex + listModel_activeProjectExpenses.quick_sort() + + storageItem.setSettings("exchangeRateModeIndex", idComboboxExchangeRateMode.currentIndex) + exchangeRateMode = idComboboxExchangeRateMode.currentIndex + + storageItem.setSettings("interativeScrollbarMode", idComboboxShowInteractiveScrollbar.currentIndex) + interativeScrollbarMode = idComboboxShowInteractiveScrollbar.currentIndex + + if (listModel_allProjects.count > 0) { // only works if a project is actually created and loaded, otherwise this gets triggered directly in BannerAddProject.qml + storageItem.setSettings("activeProjectID_unixtime", Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + activeProjectID_unixtime = Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp) + loadActiveProjectInfos_FromDB(Number(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp)) + } + + // ToDo: update base currency, if it was changed + } + + function backupProjectExpenses(selectedPath) { + listModel_tempProjectExpenses.clear() + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + var backupFileName = encodeURIComponent(listModel_allProjects.get(idComboboxProject.currentIndex).project_name) + "_backup.csv" //replaces misleading special characters with %-symbols + var backupFilePath = selectedPath + "/" + backupFileName //StandardPaths.documents + + // check if project exists + var currentProjectEntries = storageItem.getAllExpenses(listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp, "none") + if (currentProjectEntries !== "none") { + // generate temp project expenses list from chosen list + for (var i = 0; i < currentProjectEntries.length ; i++) { + listModel_tempProjectExpenses.append({ + id_unixtime_created : Number(currentProjectEntries[i][0]).toFixed(0), + date_time : Number(currentProjectEntries[i][1]).toFixed(0), + expense_name : currentProjectEntries[i][2], + expense_sum : Number(currentProjectEntries[i][3]).toFixed(2), + expense_currency : currentProjectEntries[i][4], + expense_info : currentProjectEntries[i][5], + expense_payer : currentProjectEntries[i][6], + expense_members : currentProjectEntries[i][7], + }) + } + // create string from these temp info + var toBackupString = tempProjectIndex + "\n" + + listModel_allProjects.get(idComboboxProject.currentIndex).project_name + "\n" + + "id_unixtime_created;*;date_time;*;expense_payer;*;expense_name;*;expense_sum;*;expense_currency;*;expense_members;*;expense_info" + "\n" + for (var j = 0; j < listModel_tempProjectExpenses.count; j++) { + toBackupString += listModel_tempProjectExpenses.get(j).id_unixtime_created + + ";*;" + listModel_tempProjectExpenses.get(j).date_time + + ";*;" + listModel_tempProjectExpenses.get(j).expense_payer + + ";*;" + listModel_tempProjectExpenses.get(j).expense_name + + ";*;" + listModel_tempProjectExpenses.get(j).expense_sum + + ";*;" + listModel_tempProjectExpenses.get(j).expense_currency + + ";*;" + listModel_tempProjectExpenses.get(j).expense_members + + ";*;" + listModel_tempProjectExpenses.get(j).expense_info + + "\n" + } + //console.log(toBackupString) + + // store in file and give notification + idTextFileBackup.source = backupFilePath + idTextFileBackup.text = toBackupString + notificationString = backupFilePath + + //show notification somehow on top + var headlineText = qsTr("Backup successful") + var detailText = qsTr("File saved to:") + backupFilePath + var triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + } + } + + function restoreProjectExpenses(selectedPath, selectedAction) { + idTextFileBackup.source = selectedPath + var loadTextString = (idTextFileBackup.text) + var pos = loadTextString.lastIndexOf("\n") // ToDo: remove last occurance of "\n" + var loadTextLinesArray = (loadTextString.substring(0,pos) + loadTextString.substring(pos+1)).split("\n") + var tempStringExistingId_unixtime = "" + //var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + + // plausibility check on lines 0 to 2 + if (loadTextLinesArray[2] === "id_unixtime_created;*;date_time;*;expense_payer;*;expense_name;*;expense_sum;*;expense_currency;*;expense_members;*;expense_info") { + var tempProjectIndex = listModel_allProjects.get(idComboboxProject.currentIndex).project_id_timestamp + + // if REPLACE: delete old entries in project-specific expense table in database, otherwise just MERGE + if (selectedAction === "replace") { + storageItem.deleteAllExpenses(tempProjectIndex) + } else { //"merge" + // generate expenses list for this project + var currentProjectEntries = storageItem.getAllExpenses( activeProjectID_unixtime, "none") + if (currentProjectEntries !== "none") { + for (var i = 0; i < currentProjectEntries.length ; i++) { + tempStringExistingId_unixtime += (currentProjectEntries[i][0]).toString() + ";" + } + } + //console.log(tempStringExistingId_unixtime) + } + + // fill with backup entries + for (i = 3; i < loadTextLinesArray.length; i++) { + var tempExpenseLineArray = (loadTextLinesArray[i]).split(";*;") + var project_name_table = tempProjectIndex + var id_unixtime_created = tempExpenseLineArray[0] + var date_time = tempExpenseLineArray[1] + var expense_payer = tempExpenseLineArray[2] + var expense_name = tempExpenseLineArray[3] + var expense_sum = tempExpenseLineArray[4] + var expense_currency = tempExpenseLineArray[5] + var expense_members = tempExpenseLineArray[6] + var expense_info = tempExpenseLineArray[7] + + if (selectedAction === "replace") { + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + //console.log("entering DB -> " + tempExpenseLineArray) + } else { + // check for double entries, only add if not existent + if (tempStringExistingId_unixtime.indexOf(id_unixtime_created.toString()) === -1) { + storageItem.setExpense(project_name_table, id_unixtime_created.toString(), date_time.toString(), expense_name, expense_sum, expense_currency, expense_info, expense_payer, expense_members) + //console.log("entering DB -> " + tempExpenseLineArray) + } + } + } + + //show notification somehow on top + var headlineText = qsTr("Backup successfully restored.") + if (selectedAction === "replace") { + var detailText = qsTr("Project expenses have been overwritten by backup-file expenses.") + } else { + detailText = qsTr("Project expenses have been merged with backup-file expenses.") + } + var triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + + // set this flag when the current project gets merged or replaced with a backup + if ( Number(tempProjectIndex) === Number(activeProjectID_unixtime) ) { + updateEvenWhenCanceled = true + } + } else { + //show notification somehow on top + headlineText = qsTr("Validity check failed.") + detailText = qsTr("This backup file does not seem to be created by Expenditure:") + " " + backupFilePath + triggerHiding = false + notificationBackup.showBig(headlineText, detailText) + } + } +} diff --git a/rpm/harbour-expenditure.changes.in b/rpm/harbour-expenditure.changes.in new file mode 100644 index 0000000..6eeddb9 --- /dev/null +++ b/rpm/harbour-expenditure.changes.in @@ -0,0 +1,18 @@ +# Rename this file as harbour-expenditure.changes to include changelog +# entries in your RPM file. +# +# Add new changelog entries following the format below. +# Add newest entries to the top of the list. +# Separate entries from eachother with a blank line. +# +# Alternatively, if your changelog is automatically generated (e.g. with +# the git-change-log command provided with Sailfish OS SDK), create a +# harbour-expenditure.changes.run script to let mb2 run the required commands for you. + +# * date Author's Name version-release +# - Summary of changes + +* Sun Apr 13 2014 Jack Tar 0.0.1-1 +- Scrubbed the deck +- Hoisted the sails + diff --git a/rpm/harbour-expenditure.changes.run.in b/rpm/harbour-expenditure.changes.run.in new file mode 100644 index 0000000..0d085c1 --- /dev/null +++ b/rpm/harbour-expenditure.changes.run.in @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Rename this file as harbour-expenditure.changes.run to let mb2 automatically +# generate changelog from well formatted Git commit messages and tag +# annotations. + +git-change-log + +# Here are some basic examples how to change from the default behavior. Run +# git-change-log --help inside the Sailfish OS SDK chroot or build engine to +# learn all the options git-change-log accepts. + +# Use a subset of tags +#git-change-log --tags refs/tags/my-prefix/* + +# Group entries by minor revision, suppress headlines for patch-level revisions +#git-change-log --dense '/[0-9]+.[0-9+$' + +# Trim very old changes +#git-change-log --since 2014-04-01 +#echo '[ Some changelog entries trimmed for brevity ]' + +# Use the subjects (first lines) of tag annotations when no entry would be +# included for a revision otherwise +#git-change-log --auto-add-annotations diff --git a/rpm/harbour-expenditure.spec b/rpm/harbour-expenditure.spec new file mode 100644 index 0000000..dde271b --- /dev/null +++ b/rpm/harbour-expenditure.spec @@ -0,0 +1,67 @@ +# +# Do NOT Edit the Auto-generated Part! +# Generated by: spectacle version 0.32 +# + +Name: harbour-expenditure + +# >> macros +# << macros + +Summary: Expenditure +Version: 0.2 +Release: 1 +Group: Qt/Qt +License: LICENSE +URL: http://example.org/ +Source0: %{name}-%{version}.tar.bz2 +Source100: harbour-expenditure.yaml +Requires: sailfishsilica-qt5 >= 0.10.9 +BuildRequires: pkgconfig(sailfishapp) >= 1.0.2 +BuildRequires: pkgconfig(Qt5Core) +BuildRequires: pkgconfig(Qt5Qml) +BuildRequires: pkgconfig(Qt5Quick) +BuildRequires: desktop-file-utils + +%description +Short description of my Sailfish OS Application + + +%prep +%setup -q -n %{name}-%{version} + +# >> setup +# << setup + +%build +# >> build pre +# << build pre + +%qmake5 + +make %{?_smp_mflags} + +# >> build post +# << build post + +%install +rm -rf %{buildroot} +# >> install pre +# << install pre +%qmake5_install + +# >> install post +# << install post + +desktop-file-install --delete-original \ + --dir %{buildroot}%{_datadir}/applications \ + %{buildroot}%{_datadir}/applications/*.desktop + +%files +%defattr(-,root,root,-) +%{_bindir}/%{name} +%{_datadir}/%{name} +%{_datadir}/applications/%{name}.desktop +%{_datadir}/icons/hicolor/*/apps/%{name}.png +# >> files +# << files diff --git a/rpm/harbour-expenditure.yaml b/rpm/harbour-expenditure.yaml new file mode 100644 index 0000000..a5134da --- /dev/null +++ b/rpm/harbour-expenditure.yaml @@ -0,0 +1,42 @@ +Name: harbour-expenditure +Summary: Expenditure +Version: 0.2 +Release: 1 +# The contents of the Group field should be one of the groups listed here: +# https://github.com/mer-tools/spectacle/blob/master/data/GROUPS +Group: Qt/Qt +URL: http://example.org/ +License: LICENSE +# This must be generated before uploading a package to a remote build service. +# Usually this line does not need to be modified. +Sources: +- '%{name}-%{version}.tar.bz2' +Description: | + Short description of my Sailfish OS Application +Builder: qmake5 + +# This section specifies build dependencies that are resolved using pkgconfig. +# This is the preferred way of specifying build dependencies for your package. +PkgConfigBR: + - sailfishapp >= 1.0.2 + - Qt5Core + - Qt5Qml + - Qt5Quick + +# Build dependencies without a pkgconfig setup can be listed here +# PkgBR: +# - package-needed-to-build + +# Runtime dependencies which are not automatically detected +Requires: + - sailfishsilica-qt5 >= 0.10.9 + +# All installed files +Files: + - '%{_bindir}/%{name}' + - '%{_datadir}/%{name}' + - '%{_datadir}/applications/%{name}.desktop' + - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' + +# For more information about yaml and what's supported in Sailfish OS +# build system, please see https://wiki.merproject.org/wiki/Spectacle diff --git a/src/File.cpp b/src/File.cpp new file mode 100644 index 0000000..da6c549 --- /dev/null +++ b/src/File.cpp @@ -0,0 +1,60 @@ +#include "File.h" + +#include +#include + +File::File() +{ + connect(this, SIGNAL(sourceChanged()), this, SLOT(readFile())); +} + +void File::setSource(const QString &source) +{ + m_source = source; + emit sourceChanged(); +} + +QString File::source() const +{ + return m_source; +} + +void File::setText(const QString &text) +{ + QFile file(m_source); + if (!file.open(QIODevice::WriteOnly)) { + m_text = ""; + qDebug() << "Error:" << m_source << "open failed! File not yet created."; + } + else { + qint64 byte = file.write(text.toUtf8()); + if (byte != text.toUtf8().size()) { + m_text = text.toUtf8().left(byte); + qDebug() << "Error:" << m_source << "open failed!"; + } + else { + m_text = text; + } + + file.close(); + } + + emit textChanged(); +} + +void File::readFile() +{ + QFile file(m_source); + if (!file.open(QIODevice::ReadOnly)) { + m_text = ""; + qDebug() << "Error:" << m_source << "open failed!"; + } + + m_text = file.readAll(); + emit textChanged(); +} + +QString File::text() const +{ + return m_text; +} diff --git a/src/File.h b/src/File.h new file mode 100644 index 0000000..6656c39 --- /dev/null +++ b/src/File.h @@ -0,0 +1,33 @@ +#ifndef QT_HUB_FILE_H +#define QT_HUB_FILE_H + +#include + +class File : public QObject +{ + Q_OBJECT +public: + File(); + + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + + QString source() const; + void setSource(const QString &source); + + QString text() const; + void setText(const QString &text); + +signals: + void sourceChanged(); + void textChanged(); + +private slots: + void readFile(); + +private: + QString m_source; + QString m_text; +}; + +#endif //FILE_H diff --git a/src/harbour-expenditure.cpp b/src/harbour-expenditure.cpp new file mode 100644 index 0000000..c608b23 --- /dev/null +++ b/src/harbour-expenditure.cpp @@ -0,0 +1,25 @@ +#ifdef QT_QML_DEBUG +#include +#endif + +#include + +#include // FileIO +#include "File.h" // FileIO + +int main(int argc, char *argv[]) +{ + // SailfishApp::main() will display "qml/harbour-expenditure.qml", if you need more + // control over initialization, you can use: + // + // - SailfishApp::application(int, char *[]) to get the QGuiApplication * + // - SailfishApp::createView() to get a new QQuickView * instance + // - SailfishApp::pathTo(QString) to get a QUrl to a resource file + // - SailfishApp::pathToMainQml() to get a QUrl to the main QML file + // + // To display the view, call "show()" (will show fullscreen on device). + + qmlRegisterType("FileIO", 1, 0, "TextFileIO"); // FileIO + + return SailfishApp::main(argc, argv); +} diff --git a/translations/harbour-expenditure-de.ts b/translations/harbour-expenditure-de.ts new file mode 100644 index 0000000..e8f9246 --- /dev/null +++ b/translations/harbour-expenditure-de.ts @@ -0,0 +1,430 @@ + + + + + AboutPage + + Expenditure + + + + Copyright © 2022 Tobias Planitzer + + + + License: GPL v3 + + + + Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups. + + + + Thanksgiving, feedback and support is always welcome. + + + + Troubleshooting: + + + + In case of any database error tap 10x on the word 'Settings' for cleanup options. + + + + Contact: + + + + + BannerAddExpense + + expense + + + + info + + + + payment by + + + + Save + + + + Add + + + + price + + + + currency + + + + beneficiary + + + + + BannerAddProject + + Add + + + + Project + + + + name + + + + base currency + + + + Members + + + + rename + + + + remove + + + + Save + + + + Delete this project? + + + + Clear all transactions? + + + + create + + + + edit + + + + Backup + + + + Delete + + + + Reset + + + + Restore + + + + + CalcPage + + payments + + + + saldo + + + + owes + + + + EXCHANGE RATES + + + + SPENDING OVERVIEW + + + + SETTLEMENT SUGGESTION + + + + the sum of + + + + Project: + + + + Total expenses + + + + payed + + + + received + + + + Settlement suggestion: + + + + item: + + + + payer: + + + + price: + + + + info: + + + + beneficiaries: + + + + Detailed Spendings: + + + + Expense # + + + + date: + + + + Share detailed + + + + Share compact + + + + name + + + + benefits + + + + Results + + + + constant + + + + + FirstPage + + Settings + + + + Create new project. + + + + Nothing loaded yet. + + + + Edit + + + + Remove + + + + Calculate + + + + group: + + + + Expenses + + + + Add + + + + Remove entry? + + + + + SettingsPage + + Settings + + + + Project + + + + Sorting + + + + descending + + + + ascending + + + + Exchange rate + + + + Delete stored exchange rates? + + + + Delete stored projects? + + + + Delete stored settings? + + + + per currency (constant) + + + + per transaction (dates) + + + + Restore backup file + + + + Backup to + + + + Scrollbar + + + + interactive (beta) + + + + normal + + + + Backup successful + + + + Replace + + + + Merge + + + + File saved to: + + + + Validity check failed. + + + + This backup file does not seem to be created by Expenditure: + + + + Backup successfully restored. + + + + Project expenses have been overwritten by backup-file expenses. + + + + Project expenses have been merged with backup-file expenses. + + + + Restore backup - choose action + + + + Replace deletes all former project-expenses and uses those from backup-file instead. + + + + Merge keeps former project-expenses and adds those from backup-file which are not yet on the list. + + + + File info: This backup was created by a different project. + + + + File info: This backup was created by the original project. + + + + settings + + + + exchange rates + + + + projects + + + + Database cleanup - requires restart: + + + + diff --git a/translations/harbour-expenditure.ts b/translations/harbour-expenditure.ts new file mode 100644 index 0000000..e8f9246 --- /dev/null +++ b/translations/harbour-expenditure.ts @@ -0,0 +1,430 @@ + + + + + AboutPage + + Expenditure + + + + Copyright © 2022 Tobias Planitzer + + + + License: GPL v3 + + + + Expenditure is a tool to track and split bills, project or trip expenses in multiple currencies among groups. + + + + Thanksgiving, feedback and support is always welcome. + + + + Troubleshooting: + + + + In case of any database error tap 10x on the word 'Settings' for cleanup options. + + + + Contact: + + + + + BannerAddExpense + + expense + + + + info + + + + payment by + + + + Save + + + + Add + + + + price + + + + currency + + + + beneficiary + + + + + BannerAddProject + + Add + + + + Project + + + + name + + + + base currency + + + + Members + + + + rename + + + + remove + + + + Save + + + + Delete this project? + + + + Clear all transactions? + + + + create + + + + edit + + + + Backup + + + + Delete + + + + Reset + + + + Restore + + + + + CalcPage + + payments + + + + saldo + + + + owes + + + + EXCHANGE RATES + + + + SPENDING OVERVIEW + + + + SETTLEMENT SUGGESTION + + + + the sum of + + + + Project: + + + + Total expenses + + + + payed + + + + received + + + + Settlement suggestion: + + + + item: + + + + payer: + + + + price: + + + + info: + + + + beneficiaries: + + + + Detailed Spendings: + + + + Expense # + + + + date: + + + + Share detailed + + + + Share compact + + + + name + + + + benefits + + + + Results + + + + constant + + + + + FirstPage + + Settings + + + + Create new project. + + + + Nothing loaded yet. + + + + Edit + + + + Remove + + + + Calculate + + + + group: + + + + Expenses + + + + Add + + + + Remove entry? + + + + + SettingsPage + + Settings + + + + Project + + + + Sorting + + + + descending + + + + ascending + + + + Exchange rate + + + + Delete stored exchange rates? + + + + Delete stored projects? + + + + Delete stored settings? + + + + per currency (constant) + + + + per transaction (dates) + + + + Restore backup file + + + + Backup to + + + + Scrollbar + + + + interactive (beta) + + + + normal + + + + Backup successful + + + + Replace + + + + Merge + + + + File saved to: + + + + Validity check failed. + + + + This backup file does not seem to be created by Expenditure: + + + + Backup successfully restored. + + + + Project expenses have been overwritten by backup-file expenses. + + + + Project expenses have been merged with backup-file expenses. + + + + Restore backup - choose action + + + + Replace deletes all former project-expenses and uses those from backup-file instead. + + + + Merge keeps former project-expenses and adds those from backup-file which are not yet on the list. + + + + File info: This backup was created by a different project. + + + + File info: This backup was created by the original project. + + + + settings + + + + exchange rates + + + + projects + + + + Database cleanup - requires restart: + + + +