# Changes
+## [0.21.7] = 2024-03-19
+
+* FIXES:
+ + allow user certificates in network security config
+* OTHERS:
+ + bump gradle version
+
+## [0.21.6] - 2023-06-20
+
+* FIXES
+ + fix sending transations to hledger-web 1.23+
+
+## [0.21.5] - 2022-09-03
+
+* FIXES
+ + fix cloud backup
+
+## [0.21.4] - 2022-06-18
+
+* FIXES
+ + fix compatibility wuth hledger-web 1.23+ when submitting new transactions. Thanks to Faye Duxovni for the patch!
+ + fix a crash when deleting templates
+ + fix a rare crash when submitting transactions with multiple accounts with no amounts with zero remaining balance
+
+## [0.21.3] - 2022-04-06
+
+* FIXES
+ + sync gradle version requirements
+* OTHERS
+ + bump version of several dependent libraries
+ + bump SDK version to 31
+ + adjust deprecated constructor usage
+
+## [0.21.2] - 2022-04-04
+
+* FIXES
+ + fix crash when auto-balancing multi currency transaction
+ + fix crash when duplicating template
+ + fix crash when restoring configuration backup
+* IMPROVEMENTS
+ + new transaction: turn on commodity setting when loading previous transaction with commodities
+
+## [0.21.1] - 2021-12-30
+
+* FIXES
+ + add hledger-web 1.23 support when adding transactions too
+ + correct running total when a matching transaction is added in the past
+ + fix crash when sending transaction containing only empty amounts
+
+## [0.21.0] - 2021-12-09
+
+* NEW
+ + Add support for hledger-web 1.23
+* FIXES
+ + Ship database support file missed in v0.20.4
+
+## [0.20.4] - 2021-11-18
+
+* KNOWN PROBLEMS
+ + Incompatibility with hledger-web 1.23+
+* FIXES
+ + fix auto-completion of transaction description
+
+## [0.20.3] - 2021-09-29
+
+* FIXES
+ + another fix to DB migration from v0.16.0
+
+## [0.20.2] - 2021-09-23
+
+* NEW
+ + cloud backup
+* FIXES
+ + two database problems fixed, one causing crashes at startup
+
+## [0.20.1] - 2021-09-09
+
+* FIXES
+ + New transaction: focus amount upon account selection
+ + New transaction: fix a crash when returning to the activity with no focused input field
+ + fix a crash in DB upgrade introduced in v0.20.0
+ + fix config restore with null values
+ + move away from deprecated AsyncTask
+
+## [0.20.0] - 2021-08-22
+
+* NEW
+ + backup/restore of profile/template configuration to a file
+* FIXES
+ + fix a couple of crashes related to starting new transaction via shortcut
+
+## [0.19.2] - 2020-06-09
+
+* FIXES
+ + fix auto-completion of transaction names with non-ASCII characters on some Android variants/versions (broken in 0.18.0)
+
+## [0.19.1] - 2020-05-23
+
+* FIXES
+ + fix a bug in new transaction screen when an invalid amount is entered
+ + fix loading a previous transaction by description (again)
+ + fix crash when parsing of hledger version with only two components
+
+## [0.19.0] - 2020-05-10
+
+* NEW
+ + add commodity support to the templates
+ + display running totals when filtering transaction list by account
+ + show current balance in account chooser (new transactions)
+* IMPROVEMENTS
+ + more prominent background for auto-complete pop-ups in dark mode
+ + better placement of account balances with very long/deep account names
+* FIXES
+ + honor default commodity setting in new transaction screen
+ + honor changes in currently active profile
+ + fix propagation of speculative account updates to parent accounts
+
+## [0.18.0] - 2020-05-05
+
+* NEW
+ + newly added transactions are visible in transaction list without a refresh
+* IMPROVEMENTS
+ + finished migration to fully asynchronous database layer
+ + better responsiveness when switching from the account list to the transaction list for the first time
+* FIXES
+ + fix layout glitches in template editor
+ + fix error handling while trying different JSON API versions
+ + stop resetting the date when an old transaction is loaded
+ + several smaller fixes
+
+## [0.17.1] - 2020-03-24
+
+* FIXES
+ + fix a bug in db migration for profiles without detected version
+
+## [0.17.0] - 2020-03-11
+
+* NEW
+ + transaction templates, applied via QR scan
+* IMPROVEMENTS
+ + bigger commodify button in new transaction screen
+ + unified floating action button behaviour
+ + start migration to a fully asynchronous database layer
+
+## [0.16.0] - 2020-12-28
+
+* NEW
+ + add support for latest JSON API (hledger-web 1.19.1)
+ + backend server version detection
+ + backend communication supports multiple JSON API versions
+* IMPROVEMENTS
+ + do database-related initialization in the background while the splash screen is shown
+* FIXES
+ + honour default currency in new transaction entry
+ + several crashes fixed
+
## [0.15.0] - 2020-09-20
* NEW
## Main software
-Copyright ⓒ 2018, 2019, 2020 Damyan Ivanov <dam+mole@ktnx.net>
+Copyright ⓒ 2018, 2019, 2020, 2021 Damyan Ivanov <dam+mole@ktnx.net>
MoLe is free software: you can distribute it and/or modify it
under the term of the GNU General Public License as published by
GNU General Public License terms for details.
You should have received a copy of the GNU General Public License
-along with Mobile-Ledger. If not, see <https://www.gnu.org/licenses/>.
+along with Mobile-Ledger in a file named COPYING.txt. If not, see
+<https://www.gnu.org/licenses/>.
## Libraries
See the License for details about distribution rights, and the
specific rights regarding derivate works.
-You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0
+Apache license is in Apache-2.0.txt. You may obtain a copy at:
+http://www.apache.org/licenses/LICENSE-2.0
## Other items
-Some icons taken from the Android open-source project are
-Copyright Google Inc and/or Android open-source project and licensed under the
-Apache License, version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
+Some icons taken from the Android open-source project are Copyright Google Inc
+and/or Android open-source project and licensed under the Apache License,
+version 2.0 (See Apache-2.0.txt or
+<https://www.apache.org/licenses/LICENSE-2.0>)
--- /dev/null
+* Easy way to add tag:value pairs to transactions and transaction accounts
+
+* Filter by tag:value pairs
+
+* Refresh button in account/transaction list
+
+* Top button in account/transaction list
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2023 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
apply plugin: 'com.android.application'
android {
- compileSdkVersion 29
+ compileSdkVersion 31
defaultConfig {
applicationId "net.ktnx.mobileledger"
minSdkVersion 22
- targetSdkVersion 29
+ targetSdkVersion 31
vectorDrawables.useSupportLibrary true
- versionCode 36
- versionName '0.15.0'
+ versionCode 56
+ versionName '0.21.7'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments += [
+ "room.schemaLocation" : "$projectDir/schemas".toString(),
+ "room.incremental" : "true",
+ "room.expandProjection": "true"
+ ]
+ }
+ }
}
buildTypes {
release {
versionNameSuffix '-debug'
applicationIdSuffix '.debug'
}
+ pre {
+ applicationIdSuffix '.pre'
+ versionNameSuffix '-pre'
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
}
sourceSets { main { assets.srcDirs = ['src/main/assets', 'src/main/assets/'] } }
- buildToolsVersion '29.0.3'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
productFlavors {
}
buildFeatures.viewBinding = true
+ buildToolsVersion '30.0.3'
+ namespace 'net.ktnx.mobileledger'
}
dependencies {
- def nav_version = '2.3.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
+ def room_version = '2.4.2'
+ implementation "androidx.room:room-runtime:2.4.2"
+ annotationProcessor "androidx.room:room-compiler:$room_version"
+ def nav_version = '2.4.2'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'com.google.android.material:material:1.3.0-alpha01'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- implementation 'androidx.recyclerview:recyclerview:1.1.0'
- testImplementation 'junit:junit:4.13'
- androidTestImplementation 'androidx.test:runner:1.3.0-rc01'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-rc01'
- implementation 'org.jetbrains:annotations:15.0'
- implementation 'com.fasterxml.jackson.module:jackson-modules-java8:2.11.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
+ implementation 'org.jetbrains:annotations:23.0.0'
+ implementation 'com.fasterxml.jackson.module:jackson-modules-java8:2.13.2'
implementation "androidx.navigation:navigation-fragment:$nav_version"
implementation "androidx.navigation:navigation-ui:$nav_version"
- implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
+ implementation 'androidx.appcompat:appcompat:1.6.0-alpha01'
}
allprojects {
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "6326a6bda275905c5eed9228a22612a6",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `position` INTEGER, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pattern_id` INTEGER, `acc` TEXT, `position` INTEGER, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6326a6bda275905c5eed9228a22612a6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 43,
+ "identityHash": "6326a6bda275905c5eed9228a22612a6",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `position` INTEGER, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pattern_id` INTEGER, `acc` TEXT, `position` INTEGER, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6326a6bda275905c5eed9228a22612a6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 44,
+ "identityHash": "6326a6bda275905c5eed9228a22612a6",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `position` INTEGER, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pattern_id` INTEGER, `acc` TEXT, `position` INTEGER, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6326a6bda275905c5eed9228a22612a6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 45,
+ "identityHash": "52e5cab6607fcee6f0cd8d39ba38415a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pattern_id` INTEGER, `acc` TEXT, `position` INTEGER, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '52e5cab6607fcee6f0cd8d39ba38415a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 46,
+ "identityHash": "52e5cab6607fcee6f0cd8d39ba38415a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `pattern_id` INTEGER, `acc` TEXT, `position` INTEGER, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '52e5cab6607fcee6f0cd8d39ba38415a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 47,
+ "identityHash": "ac17a190c6cd158bcb3eae9471522c6a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ac17a190c6cd158bcb3eae9471522c6a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 48,
+ "identityHash": "ac17a190c6cd158bcb3eae9471522c6a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ac17a190c6cd158bcb3eae9471522c6a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 49,
+ "identityHash": "ac17a190c6cd158bcb3eae9471522c6a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ac17a190c6cd158bcb3eae9471522c6a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 50,
+ "identityHash": "c149635e66c6c7a1e88973463ab4f35d",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` INTEGER NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c149635e66c6c7a1e88973463ab4f35d')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 51,
+ "identityHash": "d80c63258c511ee305dc5f0062a8af2a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_pattern",
+ "unique": false,
+ "columnNames": [
+ "pattern_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_pattern` ON `${TABLE_NAME}` (`pattern_id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd80c63258c511ee305dc5f0062a8af2a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 52,
+ "identityHash": "d80c63258c511ee305dc5f0062a8af2a",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_pattern",
+ "unique": false,
+ "columnNames": [
+ "pattern_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_pattern` ON `${TABLE_NAME}` (`pattern_id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd80c63258c511ee305dc5f0062a8af2a')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 53,
+ "identityHash": "be3773207074d191c145ae00acff65da",
+ "entities": [
+ {
+ "tableName": "patterns",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_patterns_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_patterns_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "pattern_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pattern_id` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`pattern_id`) REFERENCES `patterns`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "patternId",
+ "columnName": "pattern_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_pattern_accounts",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_pattern_accounts` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_pattern",
+ "unique": false,
+ "columnNames": [
+ "pattern_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_pattern` ON `${TABLE_NAME}` (`pattern_id`)"
+ },
+ {
+ "name": "fk_pattern_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_pattern_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "patterns",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "pattern_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be3773207074d191c145ae00acff65da')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 54,
+ "identityHash": "c5ddfa995546d7931ec2655613654949",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c5ddfa995546d7931ec2655613654949')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 55,
+ "identityHash": "ed75412e9453605c9829ad7f3269f62e",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ed75412e9453605c9829ad7f3269f62e')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 56,
+ "identityHash": "00c7b4a529107e23cd5925d75867f6d9",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`level` INTEGER NOT NULL, `profile` TEXT NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '00c7b4a529107e23cd5925d75867f6d9')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 57,
+ "identityHash": "5a5aa2f77594578d228d211d5e4406a6",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`level` INTEGER NOT NULL, `profile` TEXT NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5a5aa2f77594578d228d211d5e4406a6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 58,
+ "identityHash": "0f584c8b143be77895cc315ffbc41f3e",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`level` INTEGER NOT NULL, `profile` TEXT NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL, PRIMARY KEY(`uuid`))",
+ "fields": [
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "uuid"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile` TEXT NOT NULL, `account` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `account`, `currency`))",
+ "fields": [
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "account",
+ "columnName": "account",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "account",
+ "currency"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "description_history",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT NOT NULL COLLATE NOCASE, `description_upper` TEXT NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`description`))",
+ "fields": [
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "description"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile` TEXT NOT NULL, `id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `comment` TEXT, `generation` INTEGER NOT NULL, PRIMARY KEY(`profile`, `id`))",
+ "fields": [
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "un_transactions_data_hash",
+ "unique": true,
+ "columnNames": [
+ "profile",
+ "data_hash"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_data_hash` ON `${TABLE_NAME}` (`profile`, `data_hash`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile` TEXT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`profile`, `transaction_id`, `order_no`), FOREIGN KEY(`profile`, `transaction_id`) REFERENCES `transactions`(`profile`, `id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`profile`, `account_name`) REFERENCES `accounts`(`profile`, `name`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "profile",
+ "columnName": "profile",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile",
+ "transaction_id",
+ "order_no"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "fk_tran_acc_prof_acc",
+ "unique": false,
+ "columnNames": [
+ "profile",
+ "account_name"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_tran_acc_prof_acc` ON `${TABLE_NAME}` (`profile`, `account_name`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile",
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "profile",
+ "id"
+ ]
+ },
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile",
+ "account_name"
+ ],
+ "referencedColumns": [
+ "profile",
+ "name"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f584c8b143be77895cc315ffbc41f3e')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 59,
+ "identityHash": "0ab4d8a73295b6337c52ea561994b1c8",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `deprecated_uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deprecatedUUID",
+ "columnName": "deprecated_uuid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "description_history",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`description` TEXT NOT NULL COLLATE NOCASE, `description_upper` TEXT NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`description`))",
+ "fields": [
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "description"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0ab4d8a73295b6337c52ea561994b1c8')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 60,
+ "identityHash": "b52e9a5f88719ae4a44e047ce81b34ee",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `deprecated_uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deprecatedUUID",
+ "columnName": "deprecated_uuid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b52e9a5f88719ae4a44e047ce81b34ee')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 61,
+ "identityHash": "0549e893bdbb2c7eb5666c3ee81091d6",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `deprecated_uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deprecatedUUID",
+ "columnName": "deprecated_uuid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description_uc",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0549e893bdbb2c7eb5666c3ee81091d6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 62,
+ "identityHash": "69591403d82a378a35d1f22e7e8f637f",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "currency_name_idx",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `deprecated_uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deprecatedUUID",
+ "columnName": "deprecated_uuid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '69591403d82a378a35d1f22e7e8f637f')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 63,
+ "identityHash": "3a9ba5043c6e9109219046e1e29e50e1",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "templates_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "currency_name_idx",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "profiles_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3a9ba5043c6e9109219046e1e29e50e1')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 64,
+ "identityHash": "0739ea866a6aebb4217f68a7fcda5bc6",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "templates_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "currency_name_idx",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "profiles_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0739ea866a6aebb4217f68a7fcda5bc6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 65,
+ "identityHash": "0739ea866a6aebb4217f68a7fcda5bc6",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "templates_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "currency_name_idx",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "profiles_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0739ea866a6aebb4217f68a7fcda5bc6')"
+ ]
+ }
+}
\ No newline at end of file
--- /dev/null
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 66,
+ "identityHash": "0739ea866a6aebb4217f68a7fcda5bc6",
+ "entities": [
+ {
+ "tableName": "templates",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `regular_expression` TEXT NOT NULL, `test_text` TEXT, `transaction_description` TEXT, `transaction_description_match_group` INTEGER, `transaction_comment` TEXT, `transaction_comment_match_group` INTEGER, `date_year` INTEGER, `date_year_match_group` INTEGER, `date_month` INTEGER, `date_month_match_group` INTEGER, `date_day` INTEGER, `date_day_match_group` INTEGER, `is_fallback` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "regularExpression",
+ "columnName": "regular_expression",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "testText",
+ "columnName": "test_text",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescription",
+ "columnName": "transaction_description",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionDescriptionMatchGroup",
+ "columnName": "transaction_description_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionComment",
+ "columnName": "transaction_comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "transactionCommentMatchGroup",
+ "columnName": "transaction_comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYear",
+ "columnName": "date_year",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateYearMatchGroup",
+ "columnName": "date_year_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonth",
+ "columnName": "date_month",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateMonthMatchGroup",
+ "columnName": "date_month_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDay",
+ "columnName": "date_day",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateDayMatchGroup",
+ "columnName": "date_day_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isFallback",
+ "columnName": "is_fallback",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "templates_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `templates_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "template_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `template_id` INTEGER NOT NULL, `acc` TEXT, `position` INTEGER NOT NULL, `acc_match_group` INTEGER, `currency` INTEGER, `currency_match_group` INTEGER, `amount` REAL, `amount_match_group` INTEGER, `comment` TEXT, `comment_match_group` INTEGER, `negate_amount` INTEGER, FOREIGN KEY(`template_id`) REFERENCES `templates`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE , FOREIGN KEY(`currency`) REFERENCES `currencies`(`id`) ON UPDATE RESTRICT ON DELETE RESTRICT )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "templateId",
+ "columnName": "template_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "acc",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountNameMatchGroup",
+ "columnName": "acc_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currencyMatchGroup",
+ "columnName": "currency_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": false
+ },
+ {
+ "fieldPath": "amountMatchGroup",
+ "columnName": "amount_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountComment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "accountCommentMatchGroup",
+ "columnName": "comment_match_group",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "negateAmount",
+ "columnName": "negate_amount",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_template_accounts_template",
+ "unique": false,
+ "columnNames": [
+ "template_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_template` ON `${TABLE_NAME}` (`template_id`)"
+ },
+ {
+ "name": "fk_template_accounts_currency",
+ "unique": false,
+ "columnNames": [
+ "currency"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_template_accounts_currency` ON `${TABLE_NAME}` (`currency`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "templates",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "template_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ },
+ {
+ "table": "currencies",
+ "onDelete": "RESTRICT",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "currency"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "currencies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `position` TEXT NOT NULL, `has_gap` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasGap",
+ "columnName": "has_gap",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "currency_name_idx",
+ "unique": true,
+ "columnNames": [
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `currency_name_idx` ON `${TABLE_NAME}` (`name`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `profile_id` INTEGER NOT NULL, `level` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_upper` TEXT NOT NULL, `parent_name` TEXT, `expanded` INTEGER NOT NULL DEFAULT 1, `amounts_expanded` INTEGER NOT NULL DEFAULT 0, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "level",
+ "columnName": "level",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameUpper",
+ "columnName": "name_upper",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentName",
+ "columnName": "parent_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "amountsExpanded",
+ "columnName": "amounts_expanded",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_name",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_name` ON `${TABLE_NAME}` (`profile_id`, `name`)"
+ },
+ {
+ "name": "fk_account_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `uuid` TEXT NOT NULL, `url` TEXT NOT NULL, `use_authentication` INTEGER NOT NULL, `auth_user` TEXT, `auth_password` TEXT, `order_no` INTEGER NOT NULL, `permit_posting` INTEGER NOT NULL, `theme` INTEGER NOT NULL DEFAULT -1, `preferred_accounts_filter` TEXT, `future_dates` INTEGER NOT NULL, `api_version` INTEGER NOT NULL, `show_commodity_by_default` INTEGER NOT NULL, `default_commodity` TEXT, `show_comments_by_default` INTEGER NOT NULL DEFAULT 1, `detected_version_pre_1_19` INTEGER NOT NULL, `detected_version_major` INTEGER NOT NULL, `detected_version_minor` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uuid",
+ "columnName": "uuid",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "useAuthentication",
+ "columnName": "use_authentication",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authUser",
+ "columnName": "auth_user",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "authPassword",
+ "columnName": "auth_password",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permitPosting",
+ "columnName": "permit_posting",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "preferredAccountsFilter",
+ "columnName": "preferred_accounts_filter",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "futureDates",
+ "columnName": "future_dates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "apiVersion",
+ "columnName": "api_version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showCommodityByDefault",
+ "columnName": "show_commodity_by_default",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultCommodity",
+ "columnName": "default_commodity",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "showCommentsByDefault",
+ "columnName": "show_comments_by_default",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "detectedVersionPre_1_19",
+ "columnName": "detected_version_pre_1_19",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMajor",
+ "columnName": "detected_version_major",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "detectedVersionMinor",
+ "columnName": "detected_version_minor",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "profiles_uuid_idx",
+ "unique": true,
+ "columnNames": [
+ "uuid"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `profiles_uuid_idx` ON `${TABLE_NAME}` (`uuid`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "options",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profile_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profile_id`, `name`))",
+ "fields": [
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "profile_id",
+ "name"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "account_values",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `account_id` INTEGER NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `value` REAL NOT NULL, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`account_id`) REFERENCES `accounts`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountId",
+ "columnName": "account_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_account_values",
+ "unique": true,
+ "columnNames": [
+ "account_id",
+ "currency"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_account_values` ON `${TABLE_NAME}` (`account_id`, `currency`)"
+ },
+ {
+ "name": "fk_account_value_acc",
+ "unique": false,
+ "columnNames": [
+ "account_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_account_value_acc` ON `${TABLE_NAME}` (`account_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "accounts",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "account_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transactions",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ledger_id` INTEGER NOT NULL, `profile_id` INTEGER NOT NULL, `data_hash` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `day` INTEGER NOT NULL, `description` TEXT NOT NULL COLLATE NOCASE, `description_uc` TEXT NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL, FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "ledgerId",
+ "columnName": "ledger_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "profileId",
+ "columnName": "profile_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "dataHash",
+ "columnName": "data_hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "year",
+ "columnName": "year",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "month",
+ "columnName": "month",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "day",
+ "columnName": "day",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "descriptionUpper",
+ "columnName": "description_uc",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "un_transactions_ledger_id",
+ "unique": true,
+ "columnNames": [
+ "profile_id",
+ "ledger_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transactions_ledger_id` ON `${TABLE_NAME}` (`profile_id`, `ledger_id`)"
+ },
+ {
+ "name": "idx_transaction_description",
+ "unique": false,
+ "columnNames": [
+ "description"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `idx_transaction_description` ON `${TABLE_NAME}` (`description`)"
+ },
+ {
+ "name": "fk_transaction_profile",
+ "unique": false,
+ "columnNames": [
+ "profile_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_transaction_profile` ON `${TABLE_NAME}` (`profile_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "profiles",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "profile_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "transaction_accounts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `transaction_id` INTEGER NOT NULL, `order_no` INTEGER NOT NULL, `account_name` TEXT NOT NULL, `currency` TEXT NOT NULL DEFAULT '', `amount` REAL NOT NULL, `comment` TEXT, `generation` INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(`transaction_id`) REFERENCES `transactions`(`id`) ON UPDATE RESTRICT ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "transactionId",
+ "columnName": "transaction_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "orderNo",
+ "columnName": "order_no",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accountName",
+ "columnName": "account_name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "amount",
+ "columnName": "amount",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "generation",
+ "columnName": "generation",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "fk_trans_acc_trans",
+ "unique": false,
+ "columnNames": [
+ "transaction_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `fk_trans_acc_trans` ON `${TABLE_NAME}` (`transaction_id`)"
+ },
+ {
+ "name": "un_transaction_accounts",
+ "unique": true,
+ "columnNames": [
+ "transaction_id",
+ "order_no"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `un_transaction_accounts` ON `${TABLE_NAME}` (`transaction_id`, `order_no`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "transactions",
+ "onDelete": "CASCADE",
+ "onUpdate": "RESTRICT",
+ "columns": [
+ "transaction_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0739ea866a6aebb4217f68a7fcda5bc6')"
+ ]
+ }
+}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2024 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- package="net.ktnx.mobileledger">
+ xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
android:name=".App"
android:allowBackup="true"
android:appCategory="productivity"
- android:fullBackupContent="@xml/backup_descriptor"
android:icon="@drawable/app_icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@drawable/app_icon_round"
android:supportsRtl="true"
+ android:backupAgent=".backup.MobileLedgerBackupAgent"
tools:ignore="GoogleAppIndexingWarning">
+ <activity
+ android:name=".BackupsActivity"
+ android:label="@string/backups_activity_label"
+ android:theme="@style/AppTheme.default" />
+ <activity
+ android:name=".ui.templates.TemplatesActivity"
+ android:label="@string/title_activity_templates"
+ android:theme="@style/AppTheme.default" />
<activity
android:name=".ui.activity.SplashActivity"
- android:label="@string/app_name"
+ android:exported="true"
android:theme="@style/AppTheme.default">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</activity>
<activity
android:name=".ui.activity.MainActivity"
- android:label="@string/app_name"
android:theme="@style/AppTheme.default" />
<activity
- android:name=".ui.activity.NewTransactionActivity"
+ android:name=".ui.new_transaction.NewTransactionActivity"
android:label="@string/title_activity_new_transaction"
android:parentActivityName=".ui.activity.MainActivity"
- android:theme="@style/AppTheme.default" />
+ android:theme="@style/AppTheme.default"
+ android:windowSoftInputMode="stateVisible|adjustResize" />
<activity
- android:name=".ui.activity.ProfileDetailActivity"
+ android:name=".ui.profiles.ProfileDetailActivity"
android:label="@string/title_profile_details"
- android:parentActivityName=".ui.activity.MainActivity" />
+ android:parentActivityName=".ui.activity.MainActivity"
+ android:windowSoftInputMode="stateVisible|adjustResize" />
</application>
</manifest>
\ No newline at end of file
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger;
import android.app.Application;
+import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.ui.profiles.ProfileDetailModel;
+import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
import org.jetbrains.annotations.NotNull;
import java.util.Locale;
public class App extends Application {
+ public static final String PREF_NAME = "MoLe";
+ public static final String PREF_THEME_HUE = "theme-hue";
+ public static final String PREF_PROFILE_ID = "profile-id";
public static App instance;
- private MobileLedgerDatabase dbHelper;
+ private static ProfileDetailModel profileModel;
private boolean monthNamesPrepared = false;
- public static SQLiteDatabase getDatabase() {
- if (instance == null)
- throw new RuntimeException("Application not created yet");
-
- return instance.getDB();
- }
public static void prepareMonthNames() {
instance.prepareMonthNames(false);
}
+ public static void setAuthenticationDataFromProfileModel(ProfileDetailModel model) {
+ profileModel = model;
+ }
+ public static void resetAuthenticationData() {
+ profileModel = null;
+ }
+ public static void storeStartupProfileAndTheme(long currentProfileId, int currentTheme) {
+ SharedPreferences prefs = instance.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(PREF_PROFILE_ID, currentProfileId);
+ editor.putInt(PREF_THEME_HUE, currentTheme);
+ editor.apply();
+ }
+ public static long getStartupProfile() {
+ SharedPreferences prefs = instance.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ return prefs.getLong(PREF_PROFILE_ID, -1);
+ }
+ public static int getStartupTheme() {
+ SharedPreferences prefs = instance.getSharedPreferences(PREF_NAME, MODE_PRIVATE);
+ return prefs.getInt(PREF_THEME_HUE, Colors.DEFAULT_HUE_DEG);
+ }
+ private String getAuthURL() {
+ if (profileModel != null)
+ return profileModel.getUrl();
+ return Data.getProfile()
+ .getUrl();
+ }
+ private String getAuthUserName() {
+ if (profileModel != null)
+ return profileModel.getAuthUserName();
+ return Data.getProfile()
+ .getAuthUser();
+ }
+ private String getAuthPassword() {
+ if (profileModel != null)
+ return profileModel.getAuthPassword();
+ return Data.getProfile()
+ .getAuthPassword();
+ }
+ private boolean getAuthEnabled() {
+ if (profileModel != null)
+ return profileModel.getUseAuthentication();
+ return Data.getProfile()
+ .useAuthentication();
+ }
@Override
public void onCreate() {
Logger.debug("flow", "App onCreate()");
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
- MobileLedgerProfile p = Data.getProfile();
- if (p.isAuthEnabled()) {
+ if (getAuthEnabled()) {
try {
- final URL url = new URL(p.getUrl());
+ final URL url = new URL(getAuthURL());
final String requestingHost = getRequestingHost();
final String expectedHost = url.getHost();
if (requestingHost.equalsIgnoreCase(expectedHost))
- return new PasswordAuthentication(p.getAuthUserName(),
- p.getAuthPassword()
- .toCharArray());
+ return new PasswordAuthentication(getAuthUserName(),
+ getAuthPassword().toCharArray());
else
Log.w("http-auth",
String.format("Requesting host [%s] differs from expected [%s]",
monthNamesPrepared = true;
}
@Override
- public void onTerminate() {
- Logger.debug("flow", "App onTerminate()");
- if (dbHelper != null)
- dbHelper.close();
- super.onTerminate();
- }
- @Override
public void onConfigurationChanged(@NotNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
prepareMonthNames(true);
Data.refreshCurrencyData(Locale.getDefault());
Data.locale.setValue(Locale.getDefault());
}
- public SQLiteDatabase getDB() {
- if (dbHelper == null)
- initDb();
-
- return dbHelper.getWritableDatabase();
- }
- private synchronized void initDb() {
- if (dbHelper != null)
- return;
-
- dbHelper = new MobileLedgerDatabase(this);
- }
}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.google.android.material.snackbar.BaseTransientBottomBar;
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.backup.ConfigReader;
+import net.ktnx.mobileledger.backup.ConfigWriter;
+import net.ktnx.mobileledger.databinding.FragmentBackupsBinding;
+import net.ktnx.mobileledger.model.Data;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class BackupsActivity extends AppCompatActivity {
+ private FragmentBackupsBinding b;
+ private ActivityResultLauncher<String> backupChooserLauncher;
+ private ActivityResultLauncher<String[]> restoreChooserLauncher;
+ public static void start(Context context) {
+ Intent starter = new Intent(context, BackupsActivity.class);
+ context.startActivity(starter);
+ }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ b = FragmentBackupsBinding.inflate(getLayoutInflater());
+ setContentView(b.getRoot());
+
+ setSupportActionBar(b.toolbar);
+ // Show the Up button in the action bar.
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ b.backupButton.setOnClickListener(this::backupClicked);
+ b.restoreButton.setOnClickListener(this::restoreClicked);
+
+
+ backupChooserLauncher = registerForActivityResult(
+ new ActivityResultContracts.CreateDocument("application/json"), this::storeConfig);
+ restoreChooserLauncher =
+ registerForActivityResult(new ActivityResultContracts.OpenDocument(),
+ this::readConfig);
+
+ Data.observeProfile(this, p -> {
+ if (p == null) {
+ b.backupButton.setEnabled(false);
+ b.backupExplanationText.setEnabled(false);
+ }
+ else {
+ b.backupButton.setEnabled(true);
+ b.backupExplanationText.setEnabled(true);
+ }
+ });
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ private void storeConfig(Uri result) {
+ if (result == null)
+ return;
+
+ try {
+ ConfigWriter saver =
+ new ConfigWriter(getBaseContext(), result, new ConfigWriter.OnErrorListener() {
+ @Override
+ public void error(Exception e) {
+ Snackbar.make(b.backupButton, e.toString(),
+ BaseTransientBottomBar.LENGTH_LONG)
+ .show();
+ }
+ }, new ConfigWriter.OnDoneListener() {
+ public void done() {
+ Snackbar.make(b.backupButton, R.string.config_saved,
+ Snackbar.LENGTH_LONG)
+ .show();
+ }
+ });
+ saver.start();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+ private void readConfig(Uri result) {
+ if (result == null)
+ return;
+
+ try {
+ ConfigReader reader =
+ new ConfigReader(getBaseContext(), result, new ConfigWriter.OnErrorListener() {
+ @Override
+ public void error(Exception e) {
+ Snackbar.make(b.backupButton, e.toString(),
+ BaseTransientBottomBar.LENGTH_LONG)
+ .show();
+ }
+ }, new ConfigReader.OnDoneListener() {
+ public void done() {
+ Snackbar.make(b.backupButton, R.string.config_restored,
+ Snackbar.LENGTH_LONG)
+ .show();
+ }
+ });
+ reader.start();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+ private void backupClicked(View view) {
+ final Date now = new Date();
+ DateFormat df = new SimpleDateFormat("y-MM-dd HH:mm", Locale.getDefault());
+ df.format(now);
+ backupChooserLauncher.launch(String.format("MoLe-%s.json", df.format(now)));
+ }
+ private void restoreClicked(View view) {
+ restoreChooserLauncher.launch(new String[]{"application/json"});
+ }
+
+}
\ No newline at end of file
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.async;
-
-class DbOpItem {
- final String sql;
- final Object[] params;
- final Runnable onReady;
- public DbOpItem(String sql, Object[] params, Runnable onReady) {
- this.sql = sql;
- this.params = params;
- this.onReady = onReady;
- }
- public DbOpItem(String sql, Object[] params) {
- this(sql, params, null);
- }
- public DbOpItem(String sql) {
- this(sql, null, null);
- }
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.async;
-
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class DbOpQueue {
- static private final BlockingQueue<DbOpItem> queue = new LinkedBlockingQueue<>();
- static private DbOpRunner runner;
- synchronized static public void init() {
- if (runner != null)
- return;
- debug("opQueue", "Starting runner thread");
- runner = new DbOpRunner(queue);
- runner.start();
- }
- static public void done() {
- runner.interrupt();
- }
- public static void add(String sql, Object[] params) {add(sql, params, null);}
- public static void add(String sql, Object[] params, Runnable onReady) {
- init();
- debug("opQueue", "Adding " + sql);
- queue.add(new DbOpItem(sql, params, onReady));
- }
- static void add(String sql) {
- queue.add(new DbOpItem(sql));
- }
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.async;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.BuildConfig;
-
-import java.util.concurrent.BlockingQueue;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-class DbOpRunner extends Thread {
- private final BlockingQueue<DbOpItem> queue;
- public DbOpRunner(BlockingQueue<DbOpItem> queue) {
- this.queue = queue;
- }
- @Override
- public void run() {
- while (!interrupted()) {
- try {
- DbOpItem item = queue.take();
- debug("opQRunner", "Got " + item.sql);
- {
- SQLiteDatabase db = App.getDatabase();
- if (BuildConfig.DEBUG) {
- StringBuilder b = new StringBuilder("Executing ");
- b.append(item.sql);
- if (item.params.length > 0) {
- boolean first = true;
- b.append(" [");
- for (Object p : item.params) {
- if (first)
- first = false;
- else
- b.append(", ");
- b.append(p.toString());
- }
- b.append("]");
- }
- debug("opQRunner", b.toString());
- }
- db.execSQL(item.sql, item.params);
- }
- if (item.onReady != null)
- item.onReady.run();
- }
- catch (InterruptedException e) {
- break;
- }
- }
- }
-}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
public interface DescriptionSelectedCallback {
- void descriptionSelected(String description);
+ void onDescriptionSelected(String description);
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.async;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * suitable for short tasks, not involving network communication
+ */
+public class GeneralBackgroundTasks {
+ private static final Executor runner = Executors.newFixedThreadPool(Runtime.getRuntime()
+ .availableProcessors());
+ public static void run(@NotNull Runnable runnable) {
+ runner.execute(runnable);
+ }
+ public static void run(@NotNull Runnable runnable, @NotNull Runnable onSuccess) {
+ runner.execute(() -> {
+ runnable.run();
+ Misc.onMainThread(onSuccess);
+ });
+ }
+ public static void run(@NotNull Runnable runnable, @Nullable Runnable onSuccess,
+ @Nullable ErrorCallback onError, @Nullable Runnable onDone) {
+ runner.execute(() -> {
+ try {
+ runnable.run();
+ if (onSuccess != null)
+ Misc.onMainThread(onSuccess);
+ }
+ catch (Exception e) {
+ if (onError != null)
+ Misc.onMainThread(() -> onError.error(e));
+ else
+ throw e;
+ }
+ finally {
+ if (onDone != null)
+ Misc.onMainThread(onDone);
+ }
+ });
+ }
+ public static abstract class ErrorCallback {
+ abstract void error(Exception e);
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
import android.annotation.SuppressLint;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
import android.os.OperationCanceledException;
import androidx.annotation.NonNull;
-import net.ktnx.mobileledger.App;
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
+
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountWithAmounts;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Option;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
import net.ktnx.mobileledger.err.HTTPException;
-import net.ktnx.mobileledger.json.v1_15.AccountListParser;
-import net.ktnx.mobileledger.json.v1_15.ParsedBalance;
-import net.ktnx.mobileledger.json.v1_15.ParsedLedgerAccount;
-import net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction;
-import net.ktnx.mobileledger.json.v1_15.TransactionListParser;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.json.AccountListParser;
+import net.ktnx.mobileledger.json.ApiNotSupportedException;
+import net.ktnx.mobileledger.json.TransactionListParser;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.MainModel;
+import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.NetworkUtil;
import java.io.BufferedReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
-public class RetrieveTransactionsTask extends
- AsyncTask<Void, RetrieveTransactionsTask.Progress, RetrieveTransactionsTask.Result> {
+public class RetrieveTransactionsTask extends Thread {
private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
private static final Pattern reComment = Pattern.compile("^\\s*;");
private static final Pattern reTransactionStart = Pattern.compile(
private static final Pattern reEnd = Pattern.compile("\\bid=\"addmodal\"");
private static final Pattern reDecimalPoint = Pattern.compile("\\.\\d\\d?$");
private static final Pattern reDecimalComma = Pattern.compile(",\\d\\d?$");
+ private static final String TAG = "RTT";
// %3A is '='
private final Pattern reAccountName =
Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
private final Pattern reAccountValue = Pattern.compile(
"<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
- private final MainModel mainModel;
- private final MobileLedgerProfile profile;
- private final List<LedgerAccount> prevAccounts;
+ private final Profile profile;
private int expectedPostingsCount = -1;
- public RetrieveTransactionsTask(@NonNull MainModel mainModel,
- @NonNull MobileLedgerProfile profile,
- List<LedgerAccount> accounts) {
- this.mainModel = mainModel;
+ public RetrieveTransactionsTask(@NonNull Profile profile) {
this.profile = profile;
- this.prevAccounts = accounts;
}
private static void L(String msg) {
//debug("transaction-parser", msg);
return null;
}
}
- @Override
- protected void onProgressUpdate(Progress... values) {
- super.onProgressUpdate(values);
- Data.backgroundTaskProgress.postValue(values[0]);
+ private void publishProgress(Progress progress) {
+ Data.backgroundTaskProgress.postValue(progress);
}
- @Override
- protected void onPostExecute(Result result) {
- super.onPostExecute(result);
+ private void finish(Result result) {
Progress progress = new Progress();
progress.setState(ProgressState.FINISHED);
progress.setError(result.error);
- onProgressUpdate(progress);
+ publishProgress(progress);
}
- @Override
- protected void onCancelled() {
- super.onCancelled();
+ private void cancel() {
Progress progress = new Progress();
progress.setState(ProgressState.FINISHED);
- onProgressUpdate(progress);
+ publishProgress(progress);
}
private void retrieveTransactionListLegacy(List<LedgerAccount> accounts,
List<LedgerTransaction> transactions)
else {
parentAccount = null;
}
- lastAccount = new LedgerAccount(profile, accName, parentAccount);
+ lastAccount = new LedgerAccount(accName, parentAccount);
accounts.add(lastAccount);
map.put(accName, lastAccount);
state = ParserState.EXPECTING_TRANSACTION;
L(String.format("transaction %s parsed → expecting transaction",
- transaction.getId()));
+ transaction.getLedgerId()));
// sounds like a good idea, but transaction-1 may not be the first one chronologically
// for example, when you add the initial seeding transaction after entering some others
LedgerTransactionAccount lta = parseTransactionAccountLine(line);
if (lta != null) {
transaction.addAccount(lta);
- L(String.format(Locale.ENGLISH, "%d: %s = %s", transaction.getId(),
- lta.getAccountName(), lta.getAmount()));
+ L(String.format(Locale.ENGLISH, "%d: %s = %s",
+ transaction.getLedgerId(), lta.getAccountName(),
+ lta.getAmount()));
}
else
throw new IllegalStateException(
throwIfCancelled();
}
}
- private @NonNull
- LedgerAccount ensureAccountExists(String accountName, HashMap<String, LedgerAccount> map,
- ArrayList<LedgerAccount> createdAccounts) {
+ @NonNull
+ public LedgerAccount ensureAccountExists(String accountName, HashMap<String, LedgerAccount> map,
+ ArrayList<LedgerAccount> createdAccounts) {
LedgerAccount acc = map.get(accountName);
if (acc != null)
parentAccount = null;
}
- acc = new LedgerAccount(profile, accountName, parentAccount);
+ acc = new LedgerAccount(accountName, parentAccount);
createdAccounts.add(acc);
return acc;
}
- private List<LedgerAccount> retrieveAccountList() throws IOException, HTTPException {
+ public void addNumberOfPostings(int number) {
+ expectedPostingsCount += number;
+ }
+ private List<LedgerAccount> retrieveAccountList()
+ throws IOException, HTTPException, ApiNotSupportedException {
+ final API apiVersion = API.valueOf(profile.getApiVersion());
+ if (apiVersion.equals(API.auto)) {
+ return retrieveAccountListAnyVersion();
+ }
+ else if (apiVersion.equals(API.html)) {
+ Logger.debug("json",
+ "Declining using JSON API for /accounts with configured legacy API version");
+ return null;
+ }
+ else {
+ return retrieveAccountListForVersion(apiVersion);
+ }
+ }
+ private List<LedgerAccount> retrieveAccountListAnyVersion()
+ throws ApiNotSupportedException, IOException, HTTPException {
+ for (API ver : API.allVersions) {
+ try {
+ return retrieveAccountListForVersion(ver);
+ }
+ catch (JsonParseException | RuntimeJsonMappingException e) {
+ Logger.debug("json",
+ String.format(Locale.US, "Error during account list retrieval using API %s",
+ ver.getDescription()), e);
+ }
+
+ }
+
+ throw new ApiNotSupportedException();
+ }
+ private List<LedgerAccount> retrieveAccountListForVersion(API version)
+ throws IOException, HTTPException {
HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
http.setAllowUserInteraction(false);
switch (http.getResponseCode()) {
throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
}
publishProgress(Progress.indeterminate());
- SQLiteDatabase db = App.getDatabase();
ArrayList<LedgerAccount> list = new ArrayList<>();
HashMap<String, LedgerAccount> map = new HashMap<>();
- HashMap<String, LedgerAccount> currentMap = new HashMap<>();
- for (LedgerAccount acc : prevAccounts)
- currentMap.put(acc.getName(), acc);
throwIfCancelled();
try (InputStream resp = http.getInputStream()) {
throwIfCancelled();
if (http.getResponseCode() != 200)
throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
- AccountListParser parser = new AccountListParser(resp);
+ AccountListParser parser = AccountListParser.forApiVersion(version, resp);
expectedPostingsCount = 0;
while (true) {
throwIfCancelled();
- ParsedLedgerAccount parsedAccount = parser.nextAccount();
- if (parsedAccount == null) {
+ LedgerAccount acc = parser.nextAccount(this, map);
+ if (acc == null)
break;
- }
- expectedPostingsCount += parsedAccount.getAnumpostings();
- final String accName = parsedAccount.getAname();
- LedgerAccount acc = map.get(accName);
- if (acc != null)
- throw new RuntimeException(
- String.format("Account '%s' already present", acc.getName()));
- String parentName = LedgerAccount.extractParentName(accName);
- ArrayList<LedgerAccount> createdParents = new ArrayList<>();
- LedgerAccount parent;
- if (parentName == null) {
- parent = null;
- }
- else {
- parent = ensureAccountExists(parentName, map, createdParents);
- parent.setHasSubAccounts(true);
- }
- acc = new LedgerAccount(profile, accName, parent);
list.add(acc);
- map.put(accName, acc);
-
- String lastCurrency = null;
- float lastCurrencyAmount = 0;
- for (ParsedBalance b : parsedAccount.getAibalance()) {
- throwIfCancelled();
- final String currency = b.getAcommodity();
- final float amount = b.getAquantity()
- .asFloat();
- if (currency.equals(lastCurrency)) {
- lastCurrencyAmount += amount;
- }
- else {
- if (lastCurrency != null) {
- acc.addAmount(lastCurrencyAmount, lastCurrency);
- }
- lastCurrency = currency;
- lastCurrencyAmount = amount;
- }
- }
- if (lastCurrency != null) {
- acc.addAmount(lastCurrencyAmount, lastCurrency);
- }
- for (LedgerAccount p : createdParents)
- acc.propagateAmountsTo(p);
}
throwIfCancelled();
- }
- // the current account tree may have changed, update the new-to be tree to match
- for (LedgerAccount acc : list) {
- LedgerAccount prevData = currentMap.get(acc.getName());
- if (prevData != null) {
- acc.setExpanded(prevData.isExpanded());
- acc.setAmountsExpanded(prevData.amountsExpanded());
- }
+ Logger.warn("accounts",
+ String.format(Locale.US, "Got %d accounts using protocol %s", list.size(),
+ version.getDescription()));
}
return list;
}
private List<LedgerTransaction> retrieveTransactionList()
+ throws ParseException, HTTPException, IOException, ApiNotSupportedException {
+ final API apiVersion = API.valueOf(profile.getApiVersion());
+ if (apiVersion.equals(API.auto)) {
+ return retrieveTransactionListAnyVersion();
+ }
+ else if (apiVersion.equals(API.html)) {
+ Logger.debug("json",
+ "Declining using JSON API for /accounts with configured legacy API version");
+ return null;
+ }
+ else {
+ return retrieveTransactionListForVersion(apiVersion);
+ }
+
+ }
+ private List<LedgerTransaction> retrieveTransactionListAnyVersion()
+ throws ApiNotSupportedException {
+ for (API ver : API.allVersions) {
+ try {
+ return retrieveTransactionListForVersion(ver);
+ }
+ catch (Exception e) {
+ Logger.debug("json", String.format(Locale.US,
+ "Error during transaction list retrieval using API %s",
+ ver.getDescription()), e);
+ }
+
+ }
+
+ throw new ApiNotSupportedException();
+ }
+ private List<LedgerTransaction> retrieveTransactionListForVersion(API apiVersion)
throws IOException, ParseException, HTTPException {
Progress progress = new Progress();
progress.setTotal(expectedPostingsCount);
try (InputStream resp = http.getInputStream()) {
throwIfCancelled();
- TransactionListParser parser = new TransactionListParser(resp);
+ TransactionListParser parser = TransactionListParser.forApiVersion(apiVersion, resp);
int processedPostings = 0;
while (true) {
throwIfCancelled();
- ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
+ LedgerTransaction transaction = parser.nextTransaction();
throwIfCancelled();
- if (parsedTransaction == null)
+ if (transaction == null)
break;
- LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
trList.add(transaction);
progress.setProgress(processedPostings += transaction.getAccounts()
}
throwIfCancelled();
+
+ Logger.warn("transactions",
+ String.format(Locale.US, "Got %d transactions using protocol %s", trList.size(),
+ apiVersion.getDescription()));
}
- // json interface returns transactions if file order and the rest of the machinery
+ // json interface returns transactions in file order and the rest of the machinery
// expects them in reverse chronological order
Collections.sort(trList, (o1, o2) -> {
int res = o2.getDate()
.compareTo(o1.getDate());
if (res != 0)
return res;
- return Integer.compare(o2.getId(), o1.getId());
+ return Long.compare(o2.getLedgerId(), o1.getLedgerId());
});
return trList;
}
@SuppressLint("DefaultLocale")
@Override
- protected Result doInBackground(Void... params) {
+ public void run() {
Data.backgroundTaskStarted();
List<LedgerAccount> accounts;
List<LedgerTransaction> transactions;
try {
accounts = retrieveAccountList();
+ // accounts is null in API-version auto-detection and means
+ // requesting 'html' API version via the JSON classes
+ // this can't work, and the null results in the legacy code below
+ // being called
if (accounts == null)
transactions = null;
else
transactions = retrieveTransactionList();
+
if (accounts == null || transactions == null) {
accounts = new ArrayList<>();
transactions = new ArrayList<>();
retrieveTransactionListLegacy(accounts, transactions);
}
- mainModel.setAndStoreAccountAndTransactionListFromWeb(accounts, transactions);
- return new Result(accounts, transactions);
+ new AccountAndTransactionListSaver(accounts, transactions).start();
+
+ Data.lastUpdateDate.postValue(new Date());
+
+ finish(new Result(null));
}
catch (MalformedURLException e) {
e.printStackTrace();
- return new Result("Invalid server URL");
+ finish(new Result("Invalid server URL"));
}
catch (HTTPException e) {
e.printStackTrace();
- return new Result(String.format("HTTP error %d: %s", e.getResponseCode(),
- e.getResponseMessage()));
+ finish(new Result(
+ String.format("HTTP error %d: %s", e.getResponseCode(), e.getMessage())));
}
catch (IOException e) {
e.printStackTrace();
- return new Result(e.getLocalizedMessage());
+ finish(new Result(e.getLocalizedMessage()));
+ }
+ catch (RuntimeJsonMappingException e) {
+ e.printStackTrace();
+ finish(new Result(Result.ERR_JSON_PARSER_ERROR));
}
catch (ParseException e) {
e.printStackTrace();
- return new Result("Network error");
+ finish(new Result("Network error"));
}
catch (OperationCanceledException e) {
+ Logger.debug("RTT", "Retrieval was cancelled", e);
+ finish(new Result(null));
+ }
+ catch (ApiNotSupportedException e) {
e.printStackTrace();
- return new Result("Operation cancelled");
+ finish(new Result("Server version not supported"));
}
finally {
Data.backgroundTaskFinished();
}
}
- private void throwIfCancelled() {
- if (isCancelled())
+ public void throwIfCancelled() {
+ if (isInterrupted())
throw new OperationCanceledException(null);
}
private enum ParserState {
}
public static class Result {
+ public static String ERR_JSON_PARSER_ERROR = "err_json_parser";
public String error;
public List<LedgerAccount> accounts;
public List<LedgerTransaction> transactions;
this.transactions = transactions;
}
}
+
+ private class AccountAndTransactionListSaver extends Thread {
+ private final List<LedgerAccount> accounts;
+ private final List<LedgerTransaction> transactions;
+ public AccountAndTransactionListSaver(List<LedgerAccount> accounts,
+ List<LedgerTransaction> transactions) {
+ this.accounts = accounts;
+ this.transactions = transactions;
+ }
+ @Override
+ public void run() {
+ AccountDAO accDao = DB.get()
+ .getAccountDAO();
+ TransactionDAO trDao = DB.get()
+ .getTransactionDAO();
+
+ Logger.debug(TAG, "Preparing account list");
+ final List<AccountWithAmounts> list = new ArrayList<>();
+ for (LedgerAccount acc : accounts) {
+ final AccountWithAmounts a = acc.toDBOWithAmounts();
+ Account existing = accDao.getByNameSync(profile.getId(), acc.getName());
+ if (existing != null) {
+ a.account.setExpanded(existing.isExpanded());
+ a.account.setAmountsExpanded(existing.isAmountsExpanded());
+ a.account.setId(existing.getId()); // not strictly needed, but since we have it
+ // anyway...
+ }
+
+ list.add(a);
+ }
+ Logger.debug(TAG, "Account list prepared. Storing");
+ accDao.storeAccountsSync(list, profile.getId());
+ Logger.debug(TAG, "Account list stored");
+
+ Logger.debug(TAG, "Preparing transaction list");
+ final List<TransactionWithAccounts> tranList = new ArrayList<>();
+
+ for (LedgerTransaction tr : transactions)
+ tranList.add(tr.toDBO());
+
+ Logger.debug(TAG, "Storing transaction list");
+ trDao.storeTransactionsSync(tranList, profile.getId());
+
+ Logger.debug(TAG, "Transactions stored");
+
+ DB.get()
+ .getOptionDAO()
+ .insertSync(new Option(profile.getId(), Option.OPT_LAST_SCRAPE,
+ String.valueOf((new Date()).getTime())));
+ }
+ }
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2023 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
-import android.content.res.Resources;
-import android.os.AsyncTask;
-import android.util.Log;
-import android.util.SparseArray;
+import static net.ktnx.mobileledger.utils.Logger.debug;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.ObjectWriter;
+import android.util.Log;
-import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.json.ApiNotSupportedException;
+import net.ktnx.mobileledger.json.Gateway;
import net.ktnx.mobileledger.model.LedgerTransaction;
import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
import net.ktnx.mobileledger.utils.NetworkUtil;
import net.ktnx.mobileledger.utils.SimpleDate;
import net.ktnx.mobileledger.utils.UrlEncodedFormData;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import static android.os.SystemClock.sleep;
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
/* TODO: get rid of the custom session/cookie and auth code?
* (the last problem with the POST was the missing content-length header)
* This will resolve itself when hledger-web 1.14+ is released with Debian/stable,
* at which point the HTML form emulation can be dropped entirely
*/
-public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void> {
+public class SendTransactionTask extends Thread {
private final TaskCallback taskCallback;
- private final MobileLedgerProfile mProfile;
+ private final Profile mProfile;
private final boolean simulate;
+ private final LedgerTransaction transaction;
protected String error;
private String token;
private String session;
- private LedgerTransaction transaction;
- public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile,
- boolean simulate) {
+ public SendTransactionTask(TaskCallback callback, Profile profile,
+ LedgerTransaction transaction, boolean simulate) {
taskCallback = callback;
mProfile = profile;
+ this.transaction = transaction;
this.simulate = simulate;
}
- public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile) {
- taskCallback = callback;
- mProfile = profile;
- simulate = false;
- }
- private boolean send_1_15_OK() throws IOException {
+ private void sendOK(API apiVersion) throws IOException, ApiNotSupportedException {
HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
http.setRequestMethod("PUT");
http.setRequestProperty("Content-Type", "application/json");
http.setRequestProperty("Accept", "*/*");
- net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction jsonTransaction =
- net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.fromLedgerTransaction(
- transaction);
- ObjectMapper mapper = new ObjectMapper();
- ObjectWriter writer =
- mapper.writerFor(net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.class);
- String body = writer.writeValueAsString(jsonTransaction);
+ Gateway gateway = Gateway.forApiVersion(apiVersion);
+ String body = gateway.transactionSaveRequest(transaction);
- return sendRequest(http, body);
+ Logger.debug("network", "Sending using API " + apiVersion);
+ sendRequest(http, body);
}
- private boolean send_1_14_OK() throws IOException {
- HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
- http.setRequestMethod("PUT");
- http.setRequestProperty("Content-Type", "application/json");
- http.setRequestProperty("Accept", "*/*");
-
- net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction jsonTransaction =
- net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction.fromLedgerTransaction(
- transaction);
- ObjectMapper mapper = new ObjectMapper();
- ObjectWriter writer =
- mapper.writerFor(net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction.class);
- String body = writer.writeValueAsString(jsonTransaction);
-
- return sendRequest(http, body);
- }
- private boolean sendRequest(HttpURLConnection http, String body) throws IOException {
+ private void sendRequest(HttpURLConnection http, String body)
+ throws IOException, ApiNotSupportedException {
if (simulate) {
debug("network", "The request would be: " + body);
try {
Logger.debug("network", ex.toString());
}
- return true;
+ return;
}
byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
case 400:
case 405: {
BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
- String line;
+ StringBuilder errorLines = new StringBuilder();
int count = 0;
while (count <= 5) {
- line = reader.readLine();
+ String line = reader.readLine();
if (line == null)
break;
Logger.debug("network", line);
+
+ if (errorLines.length() != 0)
+ errorLines.append("\n");
+
+ errorLines.append(line);
count++;
}
- return false; // will cause a retry with the legacy method
+ throw new ApiNotSupportedException(errorLines.toString());
}
default:
BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
}
}
}
-
- return true;
}
private boolean legacySendOK() throws IOException {
HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
}
}
@Override
- protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
+ public void run() {
error = null;
try {
- transaction = ledgerTransactions[0];
-
- switch (mProfile.getApiVersion()) {
+ final API profileApiVersion = API.valueOf(mProfile.getApiVersion());
+ switch (profileApiVersion) {
case auto:
- Logger.debug("network", "Trying version 1.5.");
- if (!send_1_15_OK()) {
- Logger.debug("network", "Version 1.5 request failed. Trying with 1.14");
- if (!send_1_14_OK()) {
- Logger.debug("network",
- "Version 1.14 failed too. Trying HTML form emulation");
- legacySendOkWithRetry();
+ boolean sendOK = false;
+ for (API ver : API.allVersions) {
+ Logger.debug("network", "Trying version " + ver);
+ try {
+ sendOK(ver);
+ sendOK = true;
+ Logger.debug("network", "Version " + ver + " request succeeded");
+
+ break;
}
- else {
- Logger.debug("network", "Version 1.14 request succeeded");
+ catch (ApiNotSupportedException e) {
+ Logger.debug("network", "Version " + ver + " seems not supported");
}
}
- else {
- Logger.debug("network", "Version 1.15 request succeeded");
+
+ if (!sendOK) {
+ Logger.debug("network", "Trying HTML form emulation");
+ legacySendOkWithRetry();
}
break;
case html:
legacySendOkWithRetry();
break;
- case pre_1_15:
- send_1_14_OK();
- break;
- case post_1_14:
- send_1_15_OK();
+ case v1_14:
+ case v1_15:
+ case v1_19_1:
+ case v1_23:
+ sendOK(profileApiVersion);
break;
default:
- throw new IllegalStateException(
- "Unexpected API version: " + mProfile.getApiVersion());
+ throw new IllegalStateException("Unexpected API version: " + profileApiVersion);
}
}
- catch (Exception e) {
+ catch (ApiNotSupportedException | Exception e) {
e.printStackTrace();
error = e.getMessage();
}
- return null;
+ Misc.onMainThread(() -> taskCallback.onTransactionSaveDone(error, transaction));
}
private void legacySendOkWithRetry() throws IOException {
int tried = 0;
tried++;
if (tried >= 2)
throw new IOException(String.format("aborting after %d tries", tried));
- sleep(100);
- }
- }
- @Override
- protected void onPostExecute(Void aVoid) {
- super.onPostExecute(aVoid);
- taskCallback.done(error);
- }
-
- public enum API {
- auto(0), html(-1), pre_1_15(-2), post_1_14(-3);
- private static final SparseArray<API> map = new SparseArray<>();
-
- static {
- for (API item : API.values()) {
- map.put(item.value, item);
+ try {
+ sleep(100);
}
- }
-
- private int value;
-
- API(int value) {
- this.value = value;
- }
- public static API valueOf(int i) {
- return map.get(i, auto);
- }
- public int toInt() {
- return this.value;
- }
- public String getDescription(Resources resources) {
- switch (this) {
- case auto:
- return resources.getString(R.string.api_auto);
- case html:
- return resources.getString(R.string.api_html);
- case pre_1_15:
- return resources.getString(R.string.api_pre_1_15);
- case post_1_14:
- return resources.getString(R.string.api_post_1_14);
- default:
- throw new IllegalStateException("Unexpected value: " + value);
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ break;
}
}
}
-}
+}
\ No newline at end of file
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
public interface TaskCallback {
- void done(String error);
+ void onTransactionSaveDone(String error, Object args);
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
+import androidx.annotation.Nullable;
+
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.TransactionListItem;
import net.ktnx.mobileledger.ui.MainModel;
+import net.ktnx.mobileledger.utils.Misc;
import net.ktnx.mobileledger.utils.SimpleDate;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.util.ArrayList;
+import java.util.HashMap;
public class TransactionAccumulator {
private final ArrayList<TransactionListItem> list = new ArrayList<>();
- private final MainModel model;
+ private final String boldAccountName;
+ private final String accumulateAccount;
+ private final HashMap<String, BigDecimal> runningTotal = new HashMap<>();
private SimpleDate earliestDate, latestDate;
private SimpleDate lastDate;
- private boolean done;
private int transactionCount = 0;
- public TransactionAccumulator(MainModel model) {
- this.model = model;
+ public TransactionAccumulator(@Nullable String boldAccountName,
+ @Nullable String accumulateAccount) {
+ this.boldAccountName = boldAccountName;
+ this.accumulateAccount = accumulateAccount;
list.add(new TransactionListItem()); // head item
}
+ public void put(LedgerTransaction transaction) {
+ put(transaction, transaction.getDate());
+ }
public void put(LedgerTransaction transaction, SimpleDate date) {
- if (done)
- throw new IllegalStateException("Can't put new items after done()");
+ transactionCount++;
// first item
- if (null == latestDate)
- latestDate = date;
- earliestDate = date;
+ if (null == earliestDate)
+ earliestDate = date;
+ latestDate = date;
- if (!date.equals(lastDate)) {
- if (lastDate == null)
- lastDate = SimpleDate.today();
+ if (lastDate != null && !date.equals(lastDate)) {
boolean showMonth = date.month != lastDate.month || date.year != lastDate.year;
- list.add(new TransactionListItem(date, showMonth));
+ list.add(1, new TransactionListItem(lastDate, showMonth));
}
- list.add(new TransactionListItem(transaction));
+ String currentTotal = null;
+ if (accumulateAccount != null) {
+ for (LedgerTransactionAccount acc : transaction.getAccounts()) {
+ if (acc.getAccountName()
+ .equals(accumulateAccount) ||
+ LedgerAccount.isParentOf(accumulateAccount, acc.getAccountName()))
+ {
+ BigDecimal amt = runningTotal.get(acc.getCurrency());
+ if (amt == null)
+ amt = BigDecimal.ZERO;
+ BigDecimal newAmount = BigDecimal.valueOf(acc.getAmount());
+ newAmount = newAmount.setScale(2, RoundingMode.HALF_EVEN);
+ amt = amt.add(newAmount);
+ runningTotal.put(acc.getCurrency(), amt);
+ }
+ }
+
+ currentTotal = summarizeRunningTotal(runningTotal);
+ }
+ list.add(1, new TransactionListItem(transaction, boldAccountName, currentTotal));
lastDate = date;
- transactionCount++;
}
- public void done() {
- done = true;
+ private String summarizeRunningTotal(HashMap<String, BigDecimal> runningTotal) {
+ StringBuilder b = new StringBuilder();
+ for (String currency : runningTotal.keySet()) {
+ if (b.length() != 0)
+ b.append('\n');
+ if (Misc.emptyIsNull(currency) != null)
+ b.append(currency)
+ .append(' ');
+ BigDecimal val = runningTotal.get(currency);
+ b.append(Data.formatNumber((val == null) ? 0f : val.floatValue()));
+ }
+ return b.toString();
+ }
+ public void publishResults(MainModel model) {
+ if (lastDate != null) {
+ SimpleDate today = SimpleDate.today();
+ if (!lastDate.equals(today)) {
+ boolean showMonth = today.month != lastDate.month || today.year != lastDate.year;
+ list.add(1, new TransactionListItem(lastDate, showMonth));
+ }
+ }
+
model.setDisplayedTransactions(list, transactionCount);
model.setFirstTransactionDate(earliestDate);
model.setLastTransactionDate(latestDate);
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.async;
-import android.os.AsyncTask;
-
import net.ktnx.mobileledger.model.TransactionListItem;
import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.utils.Logger;
import java.util.Locale;
import java.util.Objects;
-public class TransactionDateFinder extends AsyncTask<TransactionDateFinder.Params, Void, Integer> {
- private MainModel model;
- @Override
- protected void onPostExecute(Integer pos) {
- model.foundTransactionItemIndex.setValue(pos);
+public class TransactionDateFinder extends Thread {
+ private final MainModel model;
+ private final SimpleDate date;
+ public TransactionDateFinder(MainModel model, SimpleDate date) {
+ this.model = model;
+ this.date = date;
}
@Override
- protected Integer doInBackground(Params... param) {
- this.model = param[0].model;
- SimpleDate date = param[0].date;
+ public void run() {
Logger.debug("go-to-date",
String.format(Locale.US, "Looking for date %04d-%02d-%02d", date.year, date.month,
date.day));
List<TransactionListItem> transactions = Objects.requireNonNull(
- param[0].model.getDisplayedTransactions()
- .getValue());
+ model.getDisplayedTransactions()
+ .getValue());
+ final int transactionCount = transactions.size();
Logger.debug("go-to-date",
- String.format(Locale.US, "List contains %d transactions", transactions.size()));
+ String.format(Locale.US, "List contains %d transactions", transactionCount));
TransactionListItem target = new TransactionListItem(date, true);
int found =
Collections.binarySearch(transactions, target, new TransactionListItemComparator());
- if (found >= 0)
- return found;
- else
- return 1 - found;
- }
+ if (found < 0)
+ found = -1 - found;
- public static class Params {
- public final MainModel model;
- public final SimpleDate date;
- public Params(@NotNull MainModel model, @NotNull SimpleDate date) {
- this.model = model;
- this.date = date;
- }
+ model.foundTransactionItemIndex.postValue(found);
}
static class TransactionListItemComparator implements Comparator<TransactionListItem> {
@Override
public int compare(@NotNull TransactionListItem a, @NotNull TransactionListItem b) {
- if (a.getType() == TransactionListItem.Type.HEADER)
+ final TransactionListItem.Type aType = a.getType();
+ if (aType == TransactionListItem.Type.HEADER)
return +1;
- if (b.getType() == TransactionListItem.Type.HEADER)
+ final TransactionListItem.Type bType = b.getType();
+ if (bType == TransactionListItem.Type.HEADER)
return -1;
final SimpleDate aDate = a.getDate();
final SimpleDate bDate = b.getDate();
if (res != 0)
return -res; // transactions are reverse sorted by date
- if (a.getType() == TransactionListItem.Type.DELIMITER) {
- if (b.getType() == TransactionListItem.Type.DELIMITER)
+ if (aType == TransactionListItem.Type.DELIMITER) {
+ if (bType == TransactionListItem.Type.DELIMITER)
return 0;
else
return -1;
}
else {
- if (b.getType() == TransactionListItem.Type.DELIMITER)
+ if (bType == TransactionListItem.Type.DELIMITER)
return +1;
else
return 0;
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.async;
-
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.MainModel;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class UpdateTransactionsTask extends AsyncTask<MainModel, Void, String> {
- protected String doInBackground(MainModel[] model) {
- final MobileLedgerProfile profile = Data.getProfile();
-
- String profile_uuid = profile.getUuid();
- Data.backgroundTaskStarted();
- try {
- String sql;
- String[] params;
-
- final String accFilter = model[0].getAccountFilter()
- .getValue();
- if (accFilter == null) {
- sql = "SELECT id, year, month, day FROM transactions WHERE profile=? ORDER BY " +
- "year desc, month desc, day desc, id desc";
- params = new String[]{profile_uuid};
-
- }
- else {
- sql = "SELECT distinct tr.id, tr.year, tr.month, tr.day from transactions tr " +
- "JOIN " + "transaction_accounts ta " +
- "ON ta.transaction_id=tr.id AND ta.profile=tr.profile WHERE tr.profile=? " +
- "and ta.account_name LIKE ?||'%' AND ta" +
- ".amount <> 0 ORDER BY tr.year desc, tr.month desc, tr.day desc, tr.id " +
- "desc";
- params = new String[]{profile_uuid, accFilter};
- }
-
- debug("UTT", sql);
- TransactionAccumulator accumulator = new TransactionAccumulator(model[0]);
-
- SQLiteDatabase db = App.getDatabase();
- try (Cursor cursor = db.rawQuery(sql, params)) {
- while (cursor.moveToNext()) {
- if (isCancelled())
- return null;
-
- accumulator.put(new LedgerTransaction(cursor.getInt(0)),
- new SimpleDate(cursor.getInt(1), cursor.getInt(2), cursor.getInt(3)));
- }
- }
-
- accumulator.done();
- debug("UTT", "transaction list value updated");
-
- return null;
- }
- finally {
- Data.backgroundTaskFinished();
- }
- }
-}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+abstract class ConfigIO extends Thread {
+ protected final OnErrorListener onErrorListener;
+ protected ParcelFileDescriptor pfd;
+ ConfigIO(Context context, Uri uri, OnErrorListener onErrorListener)
+ throws FileNotFoundException {
+ this.onErrorListener = onErrorListener;
+ pfd = context.getContentResolver()
+ .openFileDescriptor(uri, getStreamMode());
+
+ initStream();
+ }
+ abstract protected String getStreamMode();
+
+ abstract protected void initStream();
+
+ abstract protected void processStream() throws IOException;
+ @Override
+ public void run() {
+ try {
+ processStream();
+ }
+ catch (Exception e) {
+ Log.e("cfg-json", "Error processing settings as JSON", e);
+ if (onErrorListener != null)
+ Misc.onMainThread(() -> onErrorListener.error(e));
+ }
+ finally {
+ try {
+ pfd.close();
+ }
+ catch (Exception e) {
+ Log.e("cfg-json", "Error closing file descriptor", e);
+ }
+ }
+ }
+ protected static class Keys {
+ static final String ACCOUNTS = "accounts";
+ static final String AMOUNT = "amount";
+ static final String AMOUNT_GROUP = "amountGroup";
+ static final String API_VER = "apiVersion";
+ static final String AUTH_PASS = "authPass";
+ static final String AUTH_USER = "authUser";
+ static final String CAN_POST = "permitPosting";
+ static final String COLOUR = "colour";
+ static final String COMMENT = "comment";
+ static final String COMMENT_GROUP = "commentMatchGroup";
+ static final String COMMODITIES = "commodities";
+ static final String CURRENCY = "commodity";
+ static final String CURRENCY_GROUP = "commodityGroup";
+ static final String CURRENT_PROFILE = "currentProfile";
+ static final String DATE_DAY = "dateDay";
+ static final String DATE_DAY_GROUP = "dateDayMatchGroup";
+ static final String DATE_MONTH = "dateMonth";
+ static final String DATE_MONTH_GROUP = "dateMonthMatchGroup";
+ static final String DATE_YEAR = "dateYear";
+ static final String DATE_YEAR_GROUP = "dateYearMatchGroup";
+ static final String DEFAULT_COMMODITY = "defaultCommodity";
+ static final String FUTURE_DATES = "futureDates";
+ static final String HAS_GAP = "hasGap";
+ static final String IS_FALLBACK = "isFallback";
+ static final String NAME = "name";
+ static final String NAME_GROUP = "nameMatchGroup";
+ static final String NEGATE_AMOUNT = "negateAmount";
+ static final String POSITION = "position";
+ static final String PREF_ACCOUNT = "preferredAccountsFilter";
+ static final String PROFILES = "profiles";
+ static final String REGEX = "regex";
+ static final String SHOW_COMMENTS = "showCommentsByDefault";
+ static final String SHOW_COMMODITY = "showCommodityByDefault";
+ static final String TEMPLATES = "templates";
+ static final String TEST_TEXT = "testText";
+ static final String TRANSACTION = "description";
+ static final String TRANSACTION_GROUP = "descriptionMatchGroup";
+ static final String URL = "url";
+ static final String USE_AUTH = "useAuth";
+ static final String UUID = "uuid";
+ }
+
+ abstract static public class OnErrorListener {
+ public abstract void error(Exception e);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.content.Context;
+import android.net.Uri;
+
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class ConfigReader extends ConfigIO {
+ private final OnDoneListener onDoneListener;
+ private RawConfigReader r;
+ public ConfigReader(Context context, Uri uri, OnErrorListener onErrorListener,
+ OnDoneListener onDoneListener) throws FileNotFoundException {
+ super(context, uri, onErrorListener);
+
+ this.onDoneListener = onDoneListener;
+ }
+ @Override
+ protected String getStreamMode() {
+ return "r";
+ }
+ @Override
+ protected void initStream() {
+ r = new RawConfigReader(new FileInputStream(pfd.getFileDescriptor()));
+ }
+ @Override
+ protected void processStream() throws IOException {
+ r.readConfig();
+ r.restoreAll();
+ String currentProfile = r.getCurrentProfile();
+
+ if (Data.getProfile() == null) {
+ Profile p = null;
+ final ProfileDAO dao = DB.get()
+ .getProfileDAO();
+ if (currentProfile != null)
+ p = dao.getByUuidSync(currentProfile);
+
+ if (p == null)
+ dao.getAnySync();
+
+ if (p != null)
+ Data.postCurrentProfile(p);
+ }
+
+ if (onDoneListener != null)
+ Misc.onMainThread(onDoneListener::done);
+ }
+ abstract static public class OnDoneListener {
+ public abstract void done();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.content.Context;
+import android.net.Uri;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class ConfigWriter extends ConfigIO {
+ private final OnDoneListener onDoneListener;
+ private RawConfigWriter w;
+ public ConfigWriter(Context context, Uri uri, OnErrorListener onErrorListener,
+ OnDoneListener onDoneListener) throws FileNotFoundException {
+ super(context, uri, onErrorListener);
+
+ this.onDoneListener = onDoneListener;
+ }
+ @Override
+ protected String getStreamMode() {
+ return "w";
+ }
+ @Override
+ protected void initStream() {
+ w = new RawConfigWriter(new FileOutputStream(pfd.getFileDescriptor()));
+ }
+ @Override
+ protected void processStream() throws IOException {
+ w.writeConfig();
+
+ if (onDoneListener != null)
+ Misc.onMainThread(onDoneListener::done);
+ }
+ abstract static public class OnDoneListener {
+ public abstract void done();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.os.ParcelFileDescriptor;
+
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class MobileLedgerBackupAgent extends BackupAgent {
+ private static final int READ_BUF_LEN = 10;
+ public static String SETTINGS_KEY = "settings";
+ @Override
+ public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState) throws IOException {
+ Logger.debug("backup", "onBackup()");
+ backupSettings(data);
+ newState.close();
+ }
+ private void backupSettings(BackupDataOutput data) throws IOException {
+ Logger.debug ("backup", "Starting cloud backup");
+ ByteArrayOutputStream output = new ByteArrayOutputStream(4096);
+ RawConfigWriter saver = new RawConfigWriter(output);
+ saver.writeConfig();
+ byte[] bytes = output.toByteArray();
+ data.writeEntityHeader(SETTINGS_KEY, bytes.length);
+ data.writeEntityData(bytes, bytes.length);
+ Logger.debug("backup", "Done writing backup data");
+ }
+ @Override
+ public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
+ throws IOException {
+ Logger.debug("restore", "Starting cloud restore");
+ if (data.readNextHeader()) {
+ String key = data.getKey();
+ if (key.equals(SETTINGS_KEY)) {
+ restoreSettings(data);
+ }
+ }
+ }
+ private void restoreSettings(BackupDataInput data) throws IOException {
+ byte[] bytes = new byte[data.getDataSize()];
+ data.readEntityData(bytes, 0, bytes.length);
+ RawConfigReader reader = new RawConfigReader(new ByteArrayInputStream(bytes));
+ reader.readConfig();
+ Logger.debug("restore", "Successfully read restore data. Wiping database");
+ DB.get().deleteAllSync();
+ Logger.debug("restore", "Database wiped");
+ reader.restoreAll();
+ Logger.debug("restore", "All data restored from the cloud");
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.util.JsonReader;
+import android.util.JsonToken;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.backup.ConfigIO.Keys;
+import net.ktnx.mobileledger.dao.CurrencyDAO;
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.dao.TemplateHeaderDAO;
+import net.ktnx.mobileledger.db.Currency;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TemplateWithAccounts;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class RawConfigReader {
+ private final JsonReader r;
+ private List<Currency> commodities;
+ private List<Profile> profiles;
+ private List<TemplateWithAccounts> templates;
+ private String currentProfile;
+ public RawConfigReader(InputStream inputStream) {
+ r = new JsonReader(new BufferedReader(new InputStreamReader(inputStream)));
+ }
+ public List<Currency> getCommodities() {
+ return commodities;
+ }
+ public List<Profile> getProfiles() {
+ return profiles;
+ }
+ public List<TemplateWithAccounts> getTemplates() {
+ return templates;
+ }
+ public String getCurrentProfile() {
+ return currentProfile;
+ }
+ public void readConfig() throws IOException {
+ commodities = null;
+ profiles = null;
+ templates = null;
+ currentProfile = null;
+ r.beginObject();
+ while (r.hasNext()) {
+ String item = r.nextName();
+ if (r.peek() == JsonToken.NULL) {
+ r.nextNull();
+ continue;
+ }
+ switch (item) {
+ case Keys.COMMODITIES:
+ commodities = readCommodities();
+ break;
+ case Keys.PROFILES:
+ profiles = readProfiles();
+ break;
+ case Keys.TEMPLATES:
+ templates = readTemplates();
+ break;
+ case Keys.CURRENT_PROFILE:
+ currentProfile = r.nextString();
+ break;
+ default:
+ throw new RuntimeException("unexpected top-level item " + item);
+ }
+ }
+ r.endObject();
+ }
+ private TemplateAccount readTemplateAccount() throws IOException {
+ r.beginObject();
+ TemplateAccount result = new TemplateAccount(0L, 0L, 0L);
+ while (r.peek() != JsonToken.END_OBJECT) {
+ String item = r.nextName();
+ if (r.peek() == JsonToken.NULL) {
+ r.nextNull();
+ continue;
+ }
+ switch (item) {
+ case Keys.NAME:
+ result.setAccountName(r.nextString());
+ break;
+ case Keys.NAME_GROUP:
+ result.setAccountNameMatchGroup(r.nextInt());
+ break;
+ case Keys.COMMENT:
+ result.setAccountComment(r.nextString());
+ break;
+ case Keys.COMMENT_GROUP:
+ result.setAccountCommentMatchGroup(r.nextInt());
+ break;
+ case Keys.AMOUNT:
+ result.setAmount((float) r.nextDouble());
+ break;
+ case Keys.AMOUNT_GROUP:
+ result.setAmountMatchGroup(r.nextInt());
+ break;
+ case Keys.NEGATE_AMOUNT:
+ result.setNegateAmount(r.nextBoolean());
+ break;
+ case Keys.CURRENCY:
+ result.setCurrency(r.nextLong());
+ break;
+ case Keys.CURRENCY_GROUP:
+ result.setCurrencyMatchGroup(r.nextInt());
+ break;
+
+ default:
+ throw new IllegalStateException("Unexpected template account item: " + item);
+ }
+ }
+ r.endObject();
+
+ return result;
+ }
+ private TemplateWithAccounts readTemplate(JsonReader r) throws IOException {
+ r.beginObject();
+ String name = null;
+ TemplateHeader t = new TemplateHeader(0L, "", "");
+ List<TemplateAccount> accounts = new ArrayList<>();
+
+ while (r.peek() != JsonToken.END_OBJECT) {
+ String item = r.nextName();
+ if (r.peek() == JsonToken.NULL) {
+ r.nextNull();
+ continue;
+ }
+ switch (item) {
+ case Keys.UUID:
+ t.setUuid(r.nextString());
+ break;
+ case Keys.NAME:
+ t.setName(r.nextString());
+ break;
+ case Keys.REGEX:
+ t.setRegularExpression(r.nextString());
+ break;
+ case Keys.TEST_TEXT:
+ t.setTestText(r.nextString());
+ break;
+ case Keys.DATE_YEAR:
+ t.setDateYear(r.nextInt());
+ break;
+ case Keys.DATE_YEAR_GROUP:
+ t.setDateYearMatchGroup(r.nextInt());
+ break;
+ case Keys.DATE_MONTH:
+ t.setDateMonth(r.nextInt());
+ break;
+ case Keys.DATE_MONTH_GROUP:
+ t.setDateMonthMatchGroup(r.nextInt());
+ break;
+ case Keys.DATE_DAY:
+ t.setDateDay(r.nextInt());
+ break;
+ case Keys.DATE_DAY_GROUP:
+ t.setDateDayMatchGroup(r.nextInt());
+ break;
+ case Keys.TRANSACTION:
+ t.setTransactionDescription(r.nextString());
+ break;
+ case Keys.TRANSACTION_GROUP:
+ t.setTransactionDescriptionMatchGroup(r.nextInt());
+ break;
+ case Keys.COMMENT:
+ t.setTransactionComment(r.nextString());
+ break;
+ case Keys.COMMENT_GROUP:
+ t.setTransactionCommentMatchGroup(r.nextInt());
+ break;
+ case Keys.IS_FALLBACK:
+ t.setFallback(r.nextBoolean());
+ break;
+ case Keys.ACCOUNTS:
+ r.beginArray();
+ while (r.peek() == JsonToken.BEGIN_OBJECT) {
+ accounts.add(readTemplateAccount());
+ }
+ r.endArray();
+ break;
+ default:
+ throw new RuntimeException("Unknown template header item: " + item);
+ }
+ }
+ r.endObject();
+
+ TemplateWithAccounts result = new TemplateWithAccounts();
+ result.header = t;
+ result.accounts = accounts;
+ return result;
+ }
+ private List<TemplateWithAccounts> readTemplates() throws IOException {
+ List<TemplateWithAccounts> list = new ArrayList<>();
+
+ r.beginArray();
+ while (r.peek() == JsonToken.BEGIN_OBJECT) {
+ list.add(readTemplate(r));
+ }
+ r.endArray();
+
+ return list;
+ }
+ private List<Currency> readCommodities() throws IOException {
+ List<Currency> list = new ArrayList<>();
+
+ r.beginArray();
+ while (r.peek() == JsonToken.BEGIN_OBJECT) {
+ Currency c = new Currency();
+
+ r.beginObject();
+ while (r.peek() != JsonToken.END_OBJECT) {
+ final String item = r.nextName();
+ if (r.peek() == JsonToken.NULL) {
+ r.nextNull();
+ continue;
+ }
+ switch (item) {
+ case Keys.NAME:
+ c.setName(r.nextString());
+ break;
+ case Keys.POSITION:
+ c.setPosition(r.nextString());
+ break;
+ case Keys.HAS_GAP:
+ c.setHasGap(r.nextBoolean());
+ break;
+ default:
+ throw new RuntimeException("Unknown commodity key: " + item);
+ }
+ }
+ r.endObject();
+
+ if (c.getName()
+ .isEmpty())
+ throw new RuntimeException("Missing commodity name");
+
+ list.add(c);
+ }
+ r.endArray();
+
+ return list;
+ }
+ private List<Profile> readProfiles() throws IOException {
+ List<Profile> list = new ArrayList<>();
+ r.beginArray();
+ while (r.peek() == JsonToken.BEGIN_OBJECT) {
+ Profile p = new Profile();
+ r.beginObject();
+ while (r.peek() != JsonToken.END_OBJECT) {
+ String item = r.nextName();
+ if (r.peek() == JsonToken.NULL) {
+ r.nextNull();
+ continue;
+ }
+
+ switch (item) {
+ case Keys.UUID:
+ p.setUuid(r.nextString());
+ break;
+ case Keys.NAME:
+ p.setName(r.nextString());
+ break;
+ case Keys.URL:
+ p.setUrl(r.nextString());
+ break;
+ case Keys.USE_AUTH:
+ p.setUseAuthentication(r.nextBoolean());
+ break;
+ case Keys.AUTH_USER:
+ p.setAuthUser(r.nextString());
+ break;
+ case Keys.AUTH_PASS:
+ p.setAuthPassword(r.nextString());
+ break;
+ case Keys.API_VER:
+ p.setApiVersion(r.nextInt());
+ break;
+ case Keys.CAN_POST:
+ p.setPermitPosting(r.nextBoolean());
+ break;
+ case Keys.DEFAULT_COMMODITY:
+ p.setDefaultCommodity(r.nextString());
+ break;
+ case Keys.SHOW_COMMODITY:
+ p.setShowCommodityByDefault(r.nextBoolean());
+ break;
+ case Keys.SHOW_COMMENTS:
+ p.setShowCommentsByDefault(r.nextBoolean());
+ break;
+ case Keys.FUTURE_DATES:
+ p.setFutureDates(r.nextInt());
+ break;
+ case Keys.PREF_ACCOUNT:
+ p.setPreferredAccountsFilter(r.nextString());
+ break;
+ case Keys.COLOUR:
+ p.setTheme(r.nextInt());
+ break;
+
+
+ default:
+ throw new IllegalStateException("Unexpected profile item: " + item);
+ }
+ }
+ r.endObject();
+
+ list.add(p);
+ }
+ r.endArray();
+
+ return list;
+ }
+ public void restoreAll() {
+ restoreCommodities();
+ restoreProfiles();
+ restoreTemplates();
+ restoreCurrentProfile();
+ }
+ private void restoreTemplates() {
+ if (templates == null)
+ return;
+
+ TemplateHeaderDAO dao = DB.get()
+ .getTemplateDAO();
+
+ for (TemplateWithAccounts t : templates) {
+ if (dao.getTemplateWithAccountsByUuidSync(t.header.getUuid()) == null)
+ dao.insertSync(t);
+ }
+ }
+ private void restoreProfiles() {
+ if (profiles == null)
+ return;
+
+ ProfileDAO dao = DB.get()
+ .getProfileDAO();
+
+ for (Profile p : profiles) {
+ if (dao.getByUuidSync(p.getUuid()) == null)
+ dao.insert(p);
+ }
+ }
+ private void restoreCommodities() {
+ if (commodities == null)
+ return;
+
+ CurrencyDAO dao = DB.get()
+ .getCurrencyDAO();
+
+ for (Currency c : commodities) {
+ if (dao.getByNameSync(c.getName()) == null)
+ dao.insert(c);
+ }
+ }
+ private void restoreCurrentProfile() {
+ if (currentProfile == null) {
+ Logger.debug("backup", "Not restoring current profile (not present in backup)");
+ return;
+ }
+
+ ProfileDAO dao = DB.get()
+ .getProfileDAO();
+
+ Profile p = dao.getByUuidSync(currentProfile);
+
+ if (p != null) {
+ Logger.debug("backup", "Restoring current profile "+p.getName());
+ Data.postCurrentProfile(p);
+ App.storeStartupProfileAndTheme(p.getId(), p.getTheme());
+ }
+ else {
+ Logger.debug("backup", "Not restoring profile "+currentProfile+": not found in DB");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.backup;
+
+import android.util.JsonWriter;
+
+import net.ktnx.mobileledger.backup.ConfigIO.Keys;
+import net.ktnx.mobileledger.db.Currency;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateWithAccounts;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.model.Data;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class RawConfigWriter {
+ private final JsonWriter w;
+ public RawConfigWriter(OutputStream outputStream) {
+ w = new JsonWriter(new BufferedWriter(new OutputStreamWriter(outputStream)));
+ w.setIndent(" ");
+ }
+ public void writeConfig() throws IOException {
+ w.beginObject();
+ writeCommodities();
+ writeProfiles();
+ writeCurrentProfile();
+ writeConfigTemplates();
+ w.endObject();
+ w.flush();
+ }
+ private void writeKey(String key, String value) throws IOException {
+ if (value != null)
+ w.name(key)
+ .value(value);
+ }
+ private void writeKey(String key, Integer value) throws IOException {
+ if (value != null)
+ w.name(key)
+ .value(value);
+ }
+ private void writeKey(String key, Long value) throws IOException {
+ if (value != null)
+ w.name(key)
+ .value(value);
+ }
+ private void writeKey(String key, Float value) throws IOException {
+ if (value != null)
+ w.name(key)
+ .value(value);
+ }
+ private void writeKey(String key, Boolean value) throws IOException {
+ if (value != null)
+ w.name(key)
+ .value(value);
+ }
+ private void writeConfigTemplates() throws IOException {
+ List<TemplateWithAccounts> templates = DB.get()
+ .getTemplateDAO()
+ .getAllTemplatesWithAccountsSync();
+
+ if (templates.isEmpty())
+ return;
+
+ w.name("templates")
+ .beginArray();
+ for (TemplateWithAccounts t : templates) {
+ w.beginObject();
+
+ w.name(Keys.UUID)
+ .value(t.header.getUuid());
+ w.name(Keys.NAME)
+ .value(t.header.getName());
+ w.name(Keys.REGEX)
+ .value(t.header.getRegularExpression());
+ writeKey(Keys.TEST_TEXT, t.header.getTestText());
+ writeKey(ConfigIO.Keys.DATE_YEAR, t.header.getDateYear());
+ writeKey(Keys.DATE_YEAR_GROUP, t.header.getDateYearMatchGroup());
+ writeKey(Keys.DATE_MONTH, t.header.getDateMonth());
+ writeKey(Keys.DATE_MONTH_GROUP, t.header.getDateMonthMatchGroup());
+ writeKey(Keys.DATE_DAY, t.header.getDateDay());
+ writeKey(Keys.DATE_DAY_GROUP, t.header.getDateDayMatchGroup());
+ writeKey(Keys.TRANSACTION, t.header.getTransactionDescription());
+ writeKey(Keys.TRANSACTION_GROUP, t.header.getTransactionDescriptionMatchGroup());
+ writeKey(Keys.COMMENT, t.header.getTransactionComment());
+ writeKey(Keys.COMMENT_GROUP, t.header.getTransactionCommentMatchGroup());
+ w.name(Keys.IS_FALLBACK)
+ .value(t.header.isFallback());
+ if (t.accounts.size() > 0) {
+ w.name(Keys.ACCOUNTS)
+ .beginArray();
+ for (TemplateAccount a : t.accounts) {
+ w.beginObject();
+
+ writeKey(Keys.NAME, a.getAccountName());
+ writeKey(Keys.NAME_GROUP, a.getAccountNameMatchGroup());
+ writeKey(Keys.COMMENT, a.getAccountComment());
+ writeKey(Keys.COMMENT_GROUP, a.getAccountCommentMatchGroup());
+ writeKey(Keys.AMOUNT, a.getAmount());
+ writeKey(Keys.AMOUNT_GROUP, a.getAmountMatchGroup());
+ writeKey(Keys.NEGATE_AMOUNT, a.getNegateAmount());
+ writeKey(Keys.CURRENCY, a.getCurrency());
+ writeKey(Keys.CURRENCY_GROUP, a.getCurrencyMatchGroup());
+
+ w.endObject();
+ }
+ w.endArray();
+ }
+
+ w.endObject();
+ }
+ w.endArray();
+ }
+ private void writeCommodities() throws IOException {
+ List<Currency> list = DB.get()
+ .getCurrencyDAO()
+ .getAllSync();
+ if (list.isEmpty())
+ return;
+ w.name(Keys.COMMODITIES)
+ .beginArray();
+ for (Currency c : list) {
+ w.beginObject();
+ writeKey(Keys.NAME, c.getName());
+ writeKey(Keys.POSITION, c.getPosition());
+ writeKey(Keys.HAS_GAP, c.getHasGap());
+ w.endObject();
+ }
+ w.endArray();
+ }
+ private void writeProfiles() throws IOException {
+ List<Profile> profiles = DB.get()
+ .getProfileDAO()
+ .getAllOrderedSync();
+
+ if (profiles.isEmpty())
+ return;
+
+ w.name(Keys.PROFILES)
+ .beginArray();
+ for (Profile p : profiles) {
+ w.beginObject();
+
+ w.name(Keys.NAME)
+ .value(p.getName());
+ w.name(Keys.UUID)
+ .value(p.getUuid());
+ w.name(Keys.URL)
+ .value(p.getUrl());
+ w.name(Keys.USE_AUTH)
+ .value(p.useAuthentication());
+ if (p.useAuthentication()) {
+ w.name(Keys.AUTH_USER)
+ .value(p.getAuthUser());
+ w.name(Keys.AUTH_PASS)
+ .value(p.getAuthPassword());
+ }
+ if (p.getApiVersion() != API.auto.toInt())
+ w.name(Keys.API_VER)
+ .value(p.getApiVersion());
+ w.name(Keys.CAN_POST)
+ .value(p.permitPosting());
+ if (p.permitPosting()) {
+ String defaultCommodity = p.getDefaultCommodity();
+ if (!defaultCommodity.isEmpty())
+ w.name(Keys.DEFAULT_COMMODITY)
+ .value(defaultCommodity);
+ w.name(Keys.SHOW_COMMODITY)
+ .value(p.getShowCommodityByDefault());
+ w.name(Keys.SHOW_COMMENTS)
+ .value(p.getShowCommentsByDefault());
+ w.name(Keys.FUTURE_DATES)
+ .value(p.getFutureDates());
+ w.name(Keys.PREF_ACCOUNT)
+ .value(p.getPreferredAccountsFilter());
+ }
+ w.name(Keys.COLOUR)
+ .value(p.getTheme());
+
+ w.endObject();
+ }
+ w.endArray();
+ }
+ private void writeCurrentProfile() throws IOException {
+ Profile currentProfile = Data.getProfile();
+ if (currentProfile == null)
+ return;
+
+ w.name(Keys.CURRENT_PROFILE)
+ .value(currentProfile.getUuid());
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2024 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountValue;
+import net.ktnx.mobileledger.db.AccountWithAmounts;
+import net.ktnx.mobileledger.db.DB;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+@Dao
+public abstract class AccountDAO extends BaseDAO<Account> {
+ static public List<String> unbox(List<AccountNameContainer> list) {
+ ArrayList<String> result = new ArrayList<>(list.size());
+ for (AccountNameContainer item : list) {
+ result.add(item.name);
+ }
+
+ return result;
+ }
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract long insertSync(Account item);
+
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract void insertSync(List<Account> items);
+
+ @Transaction
+ public void insertSync(@NonNull AccountWithAmounts accountWithAmounts) {
+ final AccountValueDAO valueDAO = DB.get()
+ .getAccountValueDAO();
+ Account account = accountWithAmounts.account;
+ account.setId(insertSync(account));
+ for (AccountValue value : accountWithAmounts.amounts) {
+ value.setAccountId(account.getId());
+ value.setGeneration(account.getGeneration());
+ value.setId(valueDAO.insertSync(value));
+ }
+ }
+ @Update
+ public abstract void updateSync(Account item);
+
+ @Delete
+ public abstract void deleteSync(Account item);
+
+ @Delete
+ public abstract void deleteSync(List<Account> items);
+
+ @Query("DELETE FROM accounts")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM accounts WHERE profile_id=:profileId AND IIF(:includeZeroBalances=1, 1," +
+ " (EXISTS(SELECT 1 FROM account_values av WHERE av.account_id=accounts.id AND av.value" +
+ " <> 0) OR EXISTS(SELECT 1 FROM accounts a WHERE a.parent_name = accounts.name))) " +
+ "ORDER BY name")
+ public abstract LiveData<List<Account>> getAll(long profileId, boolean includeZeroBalances);
+
+ @Transaction
+ @Query("SELECT * FROM accounts WHERE profile_id = :profileId AND IIF(:includeZeroBalances=1, " +
+ "1, (EXISTS(SELECT 1 FROM account_values av WHERE av.account_id=accounts.id AND av" +
+ ".value <> 0) OR EXISTS(SELECT 1 FROM accounts a WHERE a.parent_name = accounts.name))" +
+ ") ORDER BY name")
+ public abstract LiveData<List<AccountWithAmounts>> getAllWithAmounts(long profileId,
+ boolean includeZeroBalances);
+
+ @Query("SELECT * FROM accounts WHERE id=:id")
+ public abstract Account getByIdSync(long id);
+
+ // not useful for now
+// @Transaction
+// @Query("SELECT * FROM patterns")
+// List<PatternWithAccounts> getPatternsWithAccounts();
+ @Query("SELECT * FROM accounts WHERE profile_id = :profileId AND name = :accountName")
+ public abstract LiveData<Account> getByName(long profileId, @NonNull String accountName);
+
+ @Query("SELECT * FROM accounts WHERE profile_id = :profileId AND name = :accountName")
+ public abstract Account getByNameSync(long profileId, @NonNull String accountName);
+
+ @Transaction
+ @Query("SELECT * FROM accounts WHERE profile_id = :profileId AND name = :accountName")
+ public abstract LiveData<AccountWithAmounts> getByNameWithAmounts(long profileId,
+ @NonNull String accountName);
+
+ @Query("SELECT name, CASE WHEN name_upper LIKE :term||'%%' THEN 1 " +
+ " WHEN name_upper LIKE '%%:'||:term||'%%' THEN 2 " +
+ " WHEN name_upper LIKE '%% '||:term||'%%' THEN 3 " +
+ " ELSE 9 END AS ordering " + "FROM accounts " +
+ "WHERE profile_id=:profileId AND name_upper LIKE '%%'||:term||'%%' " +
+ "ORDER BY ordering, name_upper, rowid ")
+ public abstract LiveData<List<AccountNameContainer>> lookupNamesInProfileByName(long profileId,
+ @NonNull
+ String term);
+
+ @Query("SELECT name, CASE WHEN name_upper LIKE :term||'%%' THEN 1 " +
+ " WHEN name_upper LIKE '%%:'||:term||'%%' THEN 2 " +
+ " WHEN name_upper LIKE '%% '||:term||'%%' THEN 3 " +
+ " ELSE 9 END AS ordering " + "FROM accounts " +
+ "WHERE profile_id=:profileId AND name_upper LIKE '%%'||:term||'%%' " +
+ "ORDER BY ordering, name_upper, rowid ")
+ public abstract List<AccountNameContainer> lookupNamesInProfileByNameSync(long profileId,
+ @NonNull String term);
+
+ @Transaction
+ @Query("SELECT * FROM accounts " +
+ "WHERE profile_id=:profileId AND name_upper LIKE '%%'||:term||'%%' " +
+ "ORDER BY CASE WHEN name_upper LIKE :term||'%%' THEN 1 " +
+ " WHEN name_upper LIKE '%%:'||:term||'%%' THEN 2 " +
+ " WHEN name_upper LIKE '%% '||:term||'%%' THEN 3 " +
+ " ELSE 9 END, name_upper, rowid ")
+ public abstract List<AccountWithAmounts> lookupWithAmountsInProfileByNameSync(long profileId,
+ @NonNull String term);
+
+ @Query("SELECT DISTINCT name, CASE WHEN name_upper LIKE :term||'%%' THEN 1 " +
+ " WHEN name_upper LIKE '%%:'||:term||'%%' THEN 2 " +
+ " WHEN name_upper LIKE '%% '||:term||'%%' THEN 3 " +
+ " ELSE 9 END AS ordering " + "FROM accounts " +
+ "WHERE name_upper LIKE '%%'||:term||'%%' " + "ORDER BY ordering, name_upper, rowid ")
+ public abstract LiveData<List<AccountNameContainer>> lookupNamesByName(@NonNull String term);
+
+ @Query("SELECT DISTINCT name, CASE WHEN name_upper LIKE :term||'%%' THEN 1 " +
+ " WHEN name_upper LIKE '%%:'||:term||'%%' THEN 2 " +
+ " WHEN name_upper LIKE '%% '||:term||'%%' THEN 3 " +
+ " ELSE 9 END AS ordering " + "FROM accounts " +
+ "WHERE name_upper LIKE '%%'||:term||'%%' " + "ORDER BY ordering, name_upper, rowid ")
+ public abstract List<AccountNameContainer> lookupNamesByNameSync(@NonNull String term);
+
+ @Query("SELECT * FROM accounts WHERE profile_id = :profileId")
+ public abstract List<Account> allForProfileSync(long profileId);
+
+ @Query("SELECT generation FROM accounts WHERE profile_id = :profileId LIMIT 1")
+ protected abstract AccountGenerationContainer getGenerationPOJOSync(long profileId);
+ public long getGenerationSync(long profileId) {
+ AccountGenerationContainer result = getGenerationPOJOSync(profileId);
+
+ if (result == null)
+ return 0;
+ return result.generation;
+ }
+ @Query("DELETE FROM accounts WHERE profile_id = :profileId AND generation <> " +
+ ":currentGeneration")
+ public abstract void purgeOldAccountsSync(long profileId, long currentGeneration);
+
+ @Query("DELETE FROM account_values WHERE EXISTS (SELECT 1 FROM accounts a WHERE a" +
+ ".id=account_values.account_id AND a.profile_id=:profileId) AND generation <> " +
+ ":currentGeneration")
+ public abstract void purgeOldAccountValuesSync(long profileId, long currentGeneration);
+ @Transaction
+ public void storeAccountsSync(List<AccountWithAmounts> accounts, long profileId) {
+ long generation = getGenerationSync(profileId) + 1;
+
+ for (AccountWithAmounts rec : accounts) {
+ rec.account.setGeneration(generation);
+ rec.account.setProfileId(profileId);
+ insertSync(rec);
+ }
+ purgeOldAccountsSync(profileId, generation);
+ purgeOldAccountValuesSync(profileId, generation);
+ }
+
+ static public class AccountNameContainer {
+ @ColumnInfo
+ public String name;
+ @ColumnInfo
+ public int ordering;
+ }
+
+ static class AccountGenerationContainer {
+ @ColumnInfo
+ long generation;
+ public AccountGenerationContainer(long generation) {
+ this.generation = generation;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.AccountValue;
+
+import java.util.List;
+
+@Dao
+public abstract class AccountValueDAO extends BaseDAO<AccountValue> {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract long insertSync(AccountValue item);
+
+ @Update
+ public abstract void updateSync(AccountValue item);
+
+ @Delete
+ public abstract void deleteSync(AccountValue item);
+
+ @Query("DELETE FROM account_values")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM account_values WHERE account_id=:accountId")
+ public abstract LiveData<List<AccountValue>> getAll(long accountId);
+
+ @Query("SELECT * FROM account_values WHERE account_id = :accountId AND currency = :currency")
+ public abstract AccountValue getByCurrencySync(long accountId, @NonNull String currency);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+public interface AsyncResultCallback<T extends Object> {
+ void onResult(T result);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public abstract class BaseDAO<T> {
+ private final static Executor asyncRunner = Executors.newSingleThreadExecutor();
+ public static void runAsync(Runnable runnable) {
+ asyncRunner.execute(runnable);
+ }
+ abstract long insertSync(T item);
+ public void insert(T item) {
+ asyncRunner.execute(() -> insertSync(item));
+ }
+ public void insert(T item, @NonNull OnInsertedReceiver receiver) {
+ asyncRunner.execute(() -> {
+ long id = insertSync(item);
+ Misc.onMainThread(() -> receiver.onInsert(id));
+ });
+ }
+
+ abstract void updateSync(T item);
+ public void update(T item) {
+ asyncRunner.execute(() -> updateSync(item));
+ }
+ public void update(T item, @NonNull Runnable onDone) {
+ asyncRunner.execute(() -> {
+ updateSync(item);
+ Misc.onMainThread(onDone);
+ });
+ }
+ abstract void deleteSync(T item);
+ public void delete(T item) {
+ asyncRunner.execute(() -> deleteSync(item));
+ }
+ public void delete(T item, @NonNull Runnable onDone) {
+ asyncRunner.execute(() -> {
+ deleteSync(item);
+ Misc.onMainThread(onDone);
+ });
+ }
+ interface OnInsertedReceiver {
+ void onInsert(long id);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.Currency;
+
+import java.util.List;
+
+@Dao
+public abstract class CurrencyDAO extends BaseDAO<Currency> {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract long insertSync(Currency item);
+
+ @Update
+ abstract void updateSync(Currency item);
+
+ @Delete
+ public abstract void deleteSync(Currency item);
+
+ @Query("DELETE FROM currencies")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM currencies")
+ public abstract LiveData<List<Currency>> getAll();
+
+ @Query("SELECT * FROM currencies")
+ public abstract List<Currency> getAllSync();
+
+ @Query("SELECT * FROM currencies WHERE id = :id")
+ abstract LiveData<Currency> getById(long id);
+
+ @Query("SELECT * FROM currencies WHERE id = :id")
+ public abstract Currency getByIdSync(long id);
+
+ @Query("SELECT * FROM currencies WHERE name = :name")
+ public abstract LiveData<Currency> getByName(String name);
+
+ @Query("SELECT * FROM currencies WHERE name = :name")
+ public abstract Currency getByNameSync(String name);
+
+// not useful for now
+// @Transaction
+// @Query("SELECT * FROM patterns")
+// List<PatternWithAccounts> getPatternsWithAccounts();
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.Option;
+
+import java.util.List;
+
+@Dao
+public abstract class OptionDAO extends BaseDAO<Option> {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract long insertSync(Option item);
+
+ @Update
+ public abstract void updateSync(Option item);
+
+ @Delete
+ public abstract void deleteSync(Option item);
+
+ @Delete
+ public abstract void deleteSync(List<Option> items);
+
+ @Query("DELETE from options")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM options WHERE profile_id = :profileId AND name = :name")
+ public abstract LiveData<Option> load(long profileId, String name);
+
+ @Query("SELECT * FROM options WHERE profile_id = :profileId AND name = :name")
+ public abstract Option loadSync(long profileId, String name);
+
+ @Query("SELECT * FROM options WHERE profile_id = :profileId")
+ public abstract List<Option> allForProfileSync(long profileId);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.Profile;
+
+import java.util.List;
+
+@Dao
+public abstract class ProfileDAO extends BaseDAO<Profile> {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ abstract long insertSync(Profile item);
+
+ @Transaction
+ public long insertLastSync(Profile item) {
+ int count = getProfileCountSync();
+ item.setOrderNo(count + 1);
+ return insertSync(item);
+ }
+ public void insertLast(Profile item, OnInsertedReceiver onInsertedReceiver) {
+ BaseDAO.runAsync(() -> {
+ long id = insertLastSync(item);
+ if (onInsertedReceiver != null)
+ onInsertedReceiver.onInsert(id);
+ });
+ }
+
+ @Update
+ abstract void updateSync(Profile item);
+
+ @Delete
+ public abstract void deleteSync(Profile item);
+
+ @Query("DELETE FROM profiles")
+ public abstract void deleteAllSync();
+
+ @Query("select * from profiles where id = :profileId")
+ public abstract Profile getByIdSync(long profileId);
+
+ @Query("SELECT * FROM profiles WHERE id=:profileId")
+ public abstract LiveData<Profile> getById(long profileId);
+
+ @Query("SELECT * FROM profiles ORDER BY order_no")
+ public abstract List<Profile> getAllOrderedSync();
+
+ @Query("SELECT * FROM profiles ORDER BY order_no")
+ public abstract LiveData<List<Profile>> getAllOrdered();
+
+ @Query("SELECT * FROM profiles LIMIT 1")
+ public abstract Profile getAnySync();
+
+ @Query("SELECT * FROM profiles WHERE uuid=:uuid")
+ public abstract LiveData<Profile> getByUuid(String uuid);
+
+ @Query("SELECT * FROM profiles WHERE uuid=:uuid")
+ public abstract Profile getByUuidSync(String uuid);
+
+ @Query("SELECT MAX(order_no) FROM profiles")
+ public abstract int getProfileCountSync();
+ public void updateOrderSync(List<Profile> list) {
+ if (list == null)
+ list = getAllOrderedSync();
+ int order = 1;
+ for (Profile p : list) {
+ p.setOrderNo(order++);
+ updateSync(p);
+ }
+ }
+ public void updateOrder(List<Profile> list, Runnable onDone) {
+ BaseDAO.runAsync(() -> {
+ updateOrderSync(list);
+ if (onDone != null)
+ onDone.run();
+
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.TemplateAccount;
+
+import java.util.List;
+
+@Dao
+public interface TemplateAccountDAO {
+ @Insert
+ Long insertSync(TemplateAccount item);
+
+ @Update
+ void updateSync(TemplateAccount... items);
+
+ @Delete
+ void deleteSync(TemplateAccount item);
+
+ @Query("DELETE FROM template_accounts")
+ void deleteAllSync();
+
+ @Query("SELECT * FROM template_accounts WHERE template_id=:template_id")
+ LiveData<List<TemplateAccount>> getTemplateAccounts(Long template_id);
+
+ @Query("SELECT * FROM template_accounts WHERE id = :id")
+ LiveData<TemplateAccount> getPatternAccountById(Long id);
+
+ @Query("UPDATE template_accounts set position=-1 WHERE template_id=:templateId")
+ void prepareForSave(@NonNull Long templateId);
+
+ @Query("DELETE FROM template_accounts WHERE position=-1 AND template_id=:templateId")
+ void finishSave(@NonNull Long templateId);
+}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.Observer;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.Query;
+import androidx.room.Transaction;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TemplateWithAccounts;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.List;
+
+@Dao
+public abstract class TemplateHeaderDAO {
+ @Insert()
+ public abstract long insertSync(TemplateHeader item);
+
+ public void insertAsync(@NonNull TemplateHeader item, @Nullable Runnable callback) {
+ BaseDAO.runAsync(() -> {
+ insertSync(item);
+ if (callback != null)
+ Misc.onMainThread(callback);
+ });
+ }
+
+ @Update
+ public abstract void updateSync(TemplateHeader... items);
+
+ @Delete
+ public abstract void deleteSync(TemplateHeader item);
+
+ public void deleteAsync(@NonNull TemplateHeader item, @NonNull Runnable callback) {
+ BaseDAO.runAsync(() -> {
+ deleteSync(item);
+ Misc.onMainThread(callback);
+ });
+ }
+
+ @Query("DELETE FROM templates")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM templates ORDER BY is_fallback, UPPER(name)")
+ public abstract LiveData<List<TemplateHeader>> getTemplates();
+
+ @Query("SELECT * FROM templates WHERE id = :id")
+ public abstract LiveData<TemplateHeader> getTemplate(Long id);
+
+ @Query("SELECT * FROM templates WHERE id = :id")
+ public abstract TemplateHeader getTemplateSync(Long id);
+
+ public void getTemplateAsync(@NonNull Long id,
+ @NonNull AsyncResultCallback<TemplateHeader> callback) {
+ LiveData<TemplateHeader> resultReceiver = getTemplate(id);
+ resultReceiver.observeForever(new Observer<TemplateHeader>() {
+ @Override
+ public void onChanged(TemplateHeader h) {
+ if (h == null)
+ return;
+
+ resultReceiver.removeObserver(this);
+ callback.onResult(h);
+ }
+ });
+ }
+
+ @Transaction
+ @Query("SELECT * FROM templates WHERE id = :id")
+ public abstract LiveData<TemplateWithAccounts> getTemplateWithAccounts(@NonNull Long id);
+
+ @Transaction
+ @Query("SELECT * FROM templates WHERE id = :id")
+ public abstract TemplateWithAccounts getTemplateWithAccountsSync(@NonNull Long id);
+
+ @Transaction
+ @Query("SELECT * FROM templates WHERE uuid = :uuid")
+ public abstract TemplateWithAccounts getTemplateWithAccountsByUuidSync(String uuid);
+
+ @Transaction
+ @Query("SELECT * FROM templates")
+ public abstract List<TemplateWithAccounts> getAllTemplatesWithAccountsSync();
+
+ @Transaction
+ public void insertSync(TemplateWithAccounts templateWithAccounts) {
+ long template_id = insertSync(templateWithAccounts.header);
+ for (TemplateAccount acc : templateWithAccounts.accounts) {
+ acc.setTemplateId(template_id);
+ DB.get()
+ .getTemplateAccountDAO()
+ .insertSync(acc);
+ }
+ }
+
+ public void getTemplateWithAccountsAsync(@NonNull Long id, @NonNull
+ AsyncResultCallback<TemplateWithAccounts> callback) {
+ LiveData<TemplateWithAccounts> resultReceiver = getTemplateWithAccounts(id);
+ resultReceiver.observeForever(new Observer<TemplateWithAccounts>() {
+ @Override
+ public void onChanged(TemplateWithAccounts result) {
+ if (result == null)
+ return;
+
+ resultReceiver.removeObserver(this);
+ callback.onResult(result);
+ }
+ });
+ }
+ public void insertAsync(@NonNull TemplateWithAccounts item, @Nullable Runnable callback) {
+ BaseDAO.runAsync(() -> {
+ insertSync(item);
+ if (callback != null)
+ Misc.onMainThread(callback);
+ });
+ }
+ public void duplicateTemplateWithAccounts(@NonNull Long id, @Nullable
+ AsyncResultCallback<TemplateWithAccounts> callback) {
+ BaseDAO.runAsync(() -> {
+ TemplateWithAccounts src = getTemplateWithAccountsSync(id);
+ TemplateWithAccounts dup = src.createDuplicate();
+ dup.header.setName(dup.header.getName());
+ dup.header.setId(insertSync(dup.header));
+ TemplateAccountDAO accDao = DB.get()
+ .getTemplateAccountDAO();
+ for (TemplateAccount dupAcc : dup.accounts) {
+ dupAcc.setTemplateId(dup.header.getId());
+ dupAcc.setId(accDao.insertSync(dupAcc));
+ }
+ if (callback != null)
+ Misc.onMainThread(() -> callback.onResult(dup));
+ });
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.lifecycle.LiveData;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.TransactionAccount;
+
+import java.util.List;
+
+@Dao
+public abstract class TransactionAccountDAO extends BaseDAO<TransactionAccount> {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract long insertSync(TransactionAccount item);
+
+ @Update
+ public abstract void updateSync(TransactionAccount item);
+
+ @Delete
+ public abstract void deleteSync(TransactionAccount item);
+
+ @Delete
+ public abstract void deleteSync(List<TransactionAccount> items);
+
+ @Query("DELETE FROM transaction_accounts")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM transaction_accounts WHERE id = :id")
+ public abstract LiveData<TransactionAccount> getById(long id);
+
+ @Query("SELECT * FROM transaction_accounts WHERE transaction_id = :transactionId AND order_no" +
+ " = :orderNo")
+ public abstract TransactionAccount getByOrderNoSync(long transactionId, int orderNo);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.dao;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.room.ColumnInfo;
+import androidx.room.Dao;
+import androidx.room.Delete;
+import androidx.room.Insert;
+import androidx.room.OnConflictStrategy;
+import androidx.room.Query;
+import androidx.room.Update;
+
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountValue;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Transaction;
+import net.ktnx.mobileledger.db.TransactionAccount;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+@Dao
+public abstract class TransactionDAO extends BaseDAO<Transaction> {
+ static public List<String> unbox(List<DescriptionContainer> list) {
+ ArrayList<String> result = new ArrayList<>(list.size());
+ for (DescriptionContainer item : list) {
+ result.add(item.description);
+ }
+
+ return result;
+ }
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ public abstract long insertSync(Transaction item);
+
+ @Update
+ public abstract void updateSync(Transaction item);
+
+ @Delete
+ public abstract void deleteSync(Transaction item);
+
+ @Delete
+ public abstract void deleteSync(Transaction... items);
+
+ @Delete
+ public abstract void deleteSync(List<Transaction> items);
+
+ @Query("DELETE FROM transactions")
+ public abstract void deleteAllSync();
+
+ @Query("SELECT * FROM transactions WHERE id = :id")
+ public abstract LiveData<Transaction> getById(long id);
+
+ @androidx.room.Transaction
+ @Query("SELECT * FROM transactions WHERE id = :transactionId")
+ public abstract LiveData<TransactionWithAccounts> getByIdWithAccounts(long transactionId);
+
+ @androidx.room.Transaction
+ @Query("SELECT * FROM transactions WHERE id = :transactionId")
+ public abstract TransactionWithAccounts getByIdWithAccountsSync(long transactionId);
+
+ @Query("SELECT DISTINCT description, CASE WHEN description_uc LIKE :term||'%' THEN 1 " +
+ " WHEN description_uc LIKE '%:'||:term||'%' THEN 2 " +
+ " WHEN description_uc LIKE '% '||:term||'%' THEN 3 " +
+ " ELSE 9 END AS ordering FROM transactions " +
+ "WHERE description_uc LIKE '%'||:term||'%' ORDER BY ordering, description_uc, rowid ")
+ public abstract List<DescriptionContainer> lookupDescriptionSync(@NonNull String term);
+
+ @androidx.room.Transaction
+ @Query("SELECT * from transactions WHERE description = :description ORDER BY year desc, month" +
+ " desc, day desc LIMIT 1")
+ public abstract TransactionWithAccounts getFirstByDescriptionSync(@NonNull String description);
+
+ @androidx.room.Transaction
+ @Query("SELECT tr.id, tr.profile_id, tr.ledger_id, tr.description, tr.description_uc, tr" +
+ ".data_hash, tr.comment, tr.year, tr.month, tr.day, tr.generation from transactions tr" +
+ " JOIN transaction_accounts t_a ON t_a.transaction_id = tr.id WHERE tr.description = " +
+ ":description AND t_a.account_name LIKE '%'||:accountTerm||'%' ORDER BY year desc, " +
+ "month desc, day desc, tr.ledger_id desc LIMIT 1")
+ public abstract TransactionWithAccounts getFirstByDescriptionHavingAccountSync(
+ @NonNull String description, @NonNull String accountTerm);
+
+ @Query("SELECT * from transactions WHERE profile_id = :profileId")
+ public abstract List<Transaction> getAllForProfileUnorderedSync(long profileId);
+
+ @Query("SELECT generation FROM transactions WHERE profile_id = :profileId LIMIT 1")
+ protected abstract TransactionGenerationContainer getGenerationPOJOSync(long profileId);
+
+ @androidx.room.Transaction
+ @Query("SELECT * FROM transactions WHERE profile_id = :profileId ORDER BY year " +
+ " asc, month asc, day asc, ledger_id asc")
+ public abstract LiveData<List<TransactionWithAccounts>> getAllWithAccounts(long profileId);
+
+ @androidx.room.Transaction
+ @Query("SELECT distinct(tr.id), tr.ledger_id, tr.profile_id, tr.data_hash, tr.year, tr.month," +
+ " tr.day, tr.description, tr.description_uc, tr.comment, tr.generation FROM " +
+ "transactions tr JOIN transaction_accounts ta ON ta.transaction_id=tr.id WHERE ta" +
+ ".account_name LIKE :accountName||'%' AND ta.amount <> 0 AND tr.profile_id = " +
+ ":profileId ORDER BY tr.year asc, tr.month asc, tr.day asc, tr.ledger_id asc")
+ public abstract LiveData<List<TransactionWithAccounts>> getAllWithAccountsFiltered(
+ long profileId, String accountName);
+
+ @Query("DELETE FROM transactions WHERE profile_id = :profileId AND generation <> " +
+ ":currentGeneration")
+ public abstract int purgeOldTransactionsSync(long profileId, long currentGeneration);
+
+ @Query("DELETE FROM transaction_accounts WHERE EXISTS (SELECT 1 FROM transactions tr WHERE tr" +
+ ".id=transaction_accounts.transaction_id AND tr.profile_id=:profileId) AND generation " +
+ "<> :currentGeneration")
+ public abstract int purgeOldTransactionAccountsSync(long profileId, long currentGeneration);
+
+ @Query("DELETE FROM transactions WHERE profile_id = :profileId")
+ public abstract int deleteAllSync(long profileId);
+
+ @Query("SELECT * FROM transactions where profile_id = :profileId AND ledger_id = :ledgerId")
+ public abstract Transaction getByLedgerId(long profileId, long ledgerId);
+
+ @Query("UPDATE transactions SET generation = :newGeneration WHERE id = :transactionId")
+ public abstract int updateGeneration(long transactionId, long newGeneration);
+
+ @Query("UPDATE transaction_accounts SET generation = :newGeneration WHERE transaction_id = " +
+ ":transactionId")
+ public abstract int updateAccountsGeneration(long transactionId, long newGeneration);
+
+ @Query("SELECT max(ledger_id) as ledger_id FROM transactions WHERE profile_id = :profileId")
+ public abstract LedgerIdContainer getMaxLedgerIdPOJOSync(long profileId);
+ @androidx.room.Transaction
+ public void updateGenerationWithAccounts(long transactionId, long newGeneration) {
+ updateGeneration(transactionId, newGeneration);
+ updateAccountsGeneration(transactionId, newGeneration);
+ }
+ public long getGenerationSync(long profileId) {
+ TransactionGenerationContainer result = getGenerationPOJOSync(profileId);
+
+ if (result == null)
+ return 0;
+ return result.generation;
+ }
+ public long getMaxLedgerIdSync(long profileId) {
+ LedgerIdContainer result = getMaxLedgerIdPOJOSync(profileId);
+
+ if (result == null)
+ return 0;
+ return result.ledgerId;
+ }
+ @androidx.room.Transaction
+ public void storeTransactionsSync(List<TransactionWithAccounts> list, long profileId) {
+ long generation = getGenerationSync(profileId) + 1;
+
+ for (TransactionWithAccounts tr : list) {
+ tr.transaction.setGeneration(generation);
+ tr.transaction.setProfileId(profileId);
+
+ storeSync(tr);
+ }
+
+ Logger.debug("Transaction", "Purging old transactions");
+ int removed = purgeOldTransactionsSync(profileId, generation);
+ Logger.debug("Transaction", String.format(Locale.ROOT, "Purged %d transactions", removed));
+
+ removed = purgeOldTransactionAccountsSync(profileId, generation);
+ Logger.debug("Transaction",
+ String.format(Locale.ROOT, "Purged %d transaction accounts", removed));
+ }
+ @androidx.room.Transaction
+ void storeSync(TransactionWithAccounts rec) {
+ TransactionAccountDAO trAccDao = DB.get()
+ .getTransactionAccountDAO();
+
+ Transaction transaction = rec.transaction;
+ Transaction existing = getByLedgerId(transaction.getProfileId(), transaction.getLedgerId());
+ if (existing != null) {
+ if (Misc.equalStrings(transaction.getDataHash(), existing.getDataHash())) {
+ updateGenerationWithAccounts(existing.getId(), rec.transaction.getGeneration());
+ return;
+ }
+
+ existing.copyDataFrom(transaction);
+ updateSync(existing);
+
+ transaction = existing;
+ }
+ else
+ transaction.setId(insertSync(transaction));
+
+ for (TransactionAccount trAcc : rec.accounts) {
+ trAcc.setTransactionId(transaction.getId());
+ trAcc.setGeneration(transaction.getGeneration());
+ TransactionAccount existingAcc =
+ trAccDao.getByOrderNoSync(trAcc.getTransactionId(), trAcc.getOrderNo());
+ if (existingAcc != null) {
+ existingAcc.copyDataFrom(trAcc);
+ trAccDao.updateSync(existingAcc);
+ }
+ else
+ trAcc.setId(trAccDao.insertSync(trAcc));
+ }
+ }
+ public void storeLast(TransactionWithAccounts rec) {
+ BaseDAO.runAsync(() -> appendSync(rec));
+ }
+ @androidx.room.Transaction
+ public void appendSync(TransactionWithAccounts rec) {
+ TransactionAccountDAO trAccDao = DB.get()
+ .getTransactionAccountDAO();
+ AccountDAO accDao = DB.get()
+ .getAccountDAO();
+ AccountValueDAO accValDao = DB.get()
+ .getAccountValueDAO();
+
+ Transaction transaction = rec.transaction;
+ final long profileId = transaction.getProfileId();
+ transaction.setGeneration(getGenerationSync(profileId));
+ transaction.setLedgerId(getMaxLedgerIdSync(profileId) + 1);
+ transaction.setId(insertSync(transaction));
+
+ for (TransactionAccount trAcc : rec.accounts) {
+ trAcc.setTransactionId(transaction.getId());
+ trAcc.setGeneration(transaction.getGeneration());
+ trAcc.setId(trAccDao.insertSync(trAcc));
+
+ String accName = trAcc.getAccountName();
+ while (accName != null) {
+ Account acc = accDao.getByNameSync(profileId, accName);
+ if (acc == null) {
+ acc = new Account();
+ acc.setProfileId(profileId);
+ acc.setName(accName);
+ acc.setNameUpper(accName.toUpperCase());
+ acc.setParentName(LedgerAccount.extractParentName(accName));
+ acc.setLevel(LedgerAccount.determineLevel(acc.getName()));
+ acc.setGeneration(trAcc.getGeneration());
+
+ acc.setId(accDao.insertSync(acc));
+ }
+
+ AccountValue accVal = accValDao.getByCurrencySync(acc.getId(), trAcc.getCurrency());
+ if (accVal == null) {
+ accVal = new AccountValue();
+ accVal.setAccountId(acc.getId());
+ accVal.setGeneration(trAcc.getGeneration());
+ accVal.setCurrency(trAcc.getCurrency());
+ accVal.setValue(trAcc.getAmount());
+ accVal.setId(accValDao.insertSync(accVal));
+ }
+ else {
+ accVal.setValue(accVal.getValue() + trAcc.getAmount());
+ accValDao.updateSync(accVal);
+ }
+
+ accName = LedgerAccount.extractParentName(accName);
+ }
+ }
+ }
+ static class TransactionGenerationContainer {
+ @ColumnInfo
+ long generation;
+ public TransactionGenerationContainer(long generation) {
+ this.generation = generation;
+ }
+ }
+
+ static class LedgerIdContainer {
+ @ColumnInfo(name = "ledger_id")
+ long ledgerId;
+ public LedgerIdContainer(long ledgerId) {
+ this.ledgerId = ledgerId;
+ }
+ }
+
+ static public class DescriptionContainer {
+ @ColumnInfo
+ public String description;
+ @ColumnInfo
+ public int ordering;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+@Entity(tableName = "accounts",
+ indices = {@Index(name = "un_account_name", unique = true, value = {"profile_id", "name"}),
+ @Index(name = "fk_account_profile", value = "profile_id")
+ }, foreignKeys = {
+ @ForeignKey(entity = Profile.class, parentColumns = "id", childColumns = "profile_id",
+ onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)
+})
+public class Account {
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ long id;
+ @ColumnInfo(name = "profile_id")
+ long profileId;
+ @ColumnInfo
+ int level;
+ @ColumnInfo
+ @NonNull
+ private String name;
+ @NonNull
+ @ColumnInfo(name = "name_upper")
+ private String nameUpper;
+ @ColumnInfo(name = "parent_name")
+ private String parentName;
+ @ColumnInfo(defaultValue = "1")
+ private boolean expanded = true;
+ @ColumnInfo(name = "amounts_expanded", defaultValue = "0")
+ private boolean amountsExpanded = false;
+ @ColumnInfo(defaultValue = "0")
+ private long generation;
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ public long getProfileId() {
+ return profileId;
+ }
+ public void setProfileId(long profileId) {
+ this.profileId = profileId;
+ }
+ @NonNull
+ public String getName() {
+ return name;
+ }
+ public void setName(@NonNull String name) {
+ this.name = name;
+ }
+ @NonNull
+ public String getNameUpper() {
+ return nameUpper;
+ }
+ public void setNameUpper(@NonNull String nameUpper) {
+ this.nameUpper = nameUpper;
+ }
+ public int getLevel() {
+ return level;
+ }
+ public void setLevel(int level) {
+ this.level = level;
+ }
+ public String getParentName() {
+ return parentName;
+ }
+ public void setParentName(String parentName) {
+ this.parentName = parentName;
+ }
+ public boolean isExpanded() {
+ return expanded;
+ }
+ public void setExpanded(boolean expanded) {
+ this.expanded = expanded;
+ }
+ public boolean isAmountsExpanded() {
+ return amountsExpanded;
+ }
+ public void setAmountsExpanded(boolean amountsExpanded) {
+ this.amountsExpanded = amountsExpanded;
+ }
+ public long getGeneration() {
+ return generation;
+ }
+ public void setGeneration(long generation) {
+ this.generation = generation;
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ return getName();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static net.ktnx.mobileledger.db.Profile.NO_PROFILE_ID;
+
+public class AccountAutocompleteAdapter extends ArrayAdapter<String> {
+ private final AccountFilter filter = new AccountFilter();
+ private final AccountDAO dao = DB.get()
+ .getAccountDAO();
+ private long profileId = NO_PROFILE_ID;
+ public AccountAutocompleteAdapter(Context context) {
+ super(context, android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
+ }
+ public AccountAutocompleteAdapter(Context context, @NonNull Profile profile) {
+ this(context);
+ profileId = profile.getId();
+ }
+ public void setProfileId(long profileId) {
+ this.profileId = profileId;
+ }
+ @NonNull
+ @Override
+ public Filter getFilter() {
+ return filter;
+ }
+ // @NonNull
+// @Override
+// public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+// View view = convertView;
+// if (view == null) {
+// view = LayoutInflater.from(parent.getContext())
+// .inflate(android.R.layout.simple_dropdown_item_1line, parent,
+// false);
+// }
+// Account item = getItem(position);
+// ((TextView) view.findViewById(android.R.id.text1)).setText(item.getName());
+// return view;
+// }
+ class AccountFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+ if (constraint == null) {
+ results.count = 0;
+ return results;
+ }
+
+ Logger.debug("acc", String.format("Looking for account '%s'", constraint));
+ final List<String> matches = AccountDAO.unbox(
+ (profileId == NO_PROFILE_ID) ? dao.lookupNamesByNameSync(
+ String.valueOf(constraint)
+ .toUpperCase()) : dao.lookupNamesInProfileByNameSync(profileId,
+ String.valueOf(constraint)
+ .toUpperCase()));
+ results.values = matches;
+ results.count = matches.size();
+
+ return results;
+ }
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ if (results.values == null) {
+ notifyDataSetInvalidated();
+ }
+ else {
+ setNotifyOnChange(false);
+ clear();
+ addAll((List<String>) results.values);
+ notifyDataSetChanged();
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+
+@Entity(tableName = "account_values", indices = {
+ @Index(name = "un_account_values", unique = true, value = {"account_id", "currency"}),
+ @Index(name = "fk_account_value_acc", value = "account_id")
+}, foreignKeys = {
+ @ForeignKey(entity = Account.class, parentColumns = "id", childColumns = "account_id",
+ onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)
+})
+public class AccountValue {
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ long id;
+ @ColumnInfo(name = "account_id")
+ private long accountId;
+ @NonNull
+ @ColumnInfo(defaultValue = "")
+ private String currency = "";
+ @ColumnInfo
+ private float value;
+ @ColumnInfo(defaultValue = "0")
+ private long generation = 0;
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ public long getAccountId() {
+ return accountId;
+ }
+ public void setAccountId(long accountId) {
+ this.accountId = accountId;
+ }
+ @NonNull
+ public String getCurrency() {
+ return currency;
+ }
+ public void setCurrency(@NonNull String currency) {
+ this.currency = currency;
+ }
+ public float getValue() {
+ return value;
+ }
+ public void setValue(float value) {
+ this.value = value;
+ }
+ public long getGeneration() {
+ return generation;
+ }
+ public void setGeneration(long generation) {
+ this.generation = generation;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class AccountWithAmounts {
+ @Embedded
+ public Account account;
+ @Relation(parentColumn = "id", entityColumn = "account_id")
+ public List<AccountValue> amounts;
+ @NonNull
+ @Override
+ public String toString() {
+ return account.getName();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.List;
+
+public class AccountWithAmountsAutocompleteAdapter extends ArrayAdapter<AccountWithAmounts> {
+ private final AccountFilter filter = new AccountFilter();
+ private final long profileId;
+ public AccountWithAmountsAutocompleteAdapter(Context context, @NonNull Profile profile) {
+ super(context, R.layout.account_autocomplete_row);
+ profileId = profile.getId();
+ }
+ @NonNull
+ @Override
+ public Filter getFilter() {
+ return filter;
+ }
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.account_autocomplete_row, parent, false);
+ }
+ AccountWithAmounts item = getItem(position);
+ ((TextView) view.findViewById(R.id.account_name)).setText(item.account.getName());
+ StringBuilder amountsText = new StringBuilder();
+ for (AccountValue amt : item.amounts) {
+ if (amountsText.length() != 0)
+ amountsText.append('\n');
+ String currency = amt.getCurrency();
+ if (Misc.emptyIsNull(currency) != null)
+ amountsText.append(currency)
+ .append(' ');
+ amountsText.append(Data.formatNumber(amt.getValue()));
+ }
+ ((TextView) view.findViewById(R.id.amounts)).setText(amountsText.toString());
+
+ return view;
+ }
+ class AccountFilter extends Filter {
+ private final AccountDAO dao = DB.get()
+ .getAccountDAO();
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+ if (constraint == null) {
+ results.count = 0;
+ return results;
+ }
+
+ Logger.debug("acc", String.format("Looking for account '%s'", constraint));
+ final List<AccountWithAmounts> matches =
+ dao.lookupWithAmountsInProfileByNameSync(profileId, String.valueOf(constraint)
+ .toUpperCase());
+ results.values = matches;
+ results.count = matches.size();
+
+ return results;
+ }
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ if (results.values == null) {
+ notifyDataSetInvalidated();
+ }
+ else {
+ setNotifyOnChange(false);
+ clear();
+ addAll((List<AccountWithAmounts>) results.values);
+ notifyDataSetChanged();
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Ignore;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+@Entity(tableName = "currencies",
+ indices = {@Index(name = "currency_name_idx", unique = true, value = "name")})
+public class Currency {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @NonNull
+ private String name;
+ @NonNull
+ private String position;
+ @NonNull
+ @ColumnInfo(name = "has_gap")
+ private Boolean hasGap;
+ @Ignore
+ public Currency() {
+ id = 0;
+ name = "";
+ position = "after";
+ hasGap = true;
+ }
+ public Currency(long id, @NonNull String name, @NonNull String position,
+ @NonNull Boolean hasGap) {
+ this.id = id;
+ this.name = name;
+ this.position = position;
+ this.hasGap = hasGap;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ @NonNull
+ public String getName() {
+ return name;
+ }
+ public void setName(@NonNull String name) {
+ this.name = name;
+ }
+ @NonNull
+ public String getPosition() {
+ return position;
+ }
+ public void setPosition(@NonNull String position) {
+ this.position = position;
+ }
+ @NonNull
+ public Boolean getHasGap() {
+ return hasGap;
+ }
+ public void setHasGap(@NonNull Boolean hasGap) {
+ this.hasGap = hasGap;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.SQLException;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.MutableLiveData;
+import androidx.room.Database;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.dao.AccountValueDAO;
+import net.ktnx.mobileledger.dao.CurrencyDAO;
+import net.ktnx.mobileledger.dao.OptionDAO;
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.dao.TemplateAccountDAO;
+import net.ktnx.mobileledger.dao.TemplateHeaderDAO;
+import net.ktnx.mobileledger.dao.TransactionAccountDAO;
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.utils.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+@Database(version = DB.REVISION,
+ entities = {TemplateHeader.class, TemplateAccount.class, Currency.class, Account.class,
+ Profile.class, Option.class, AccountValue.class, Transaction.class,
+ TransactionAccount.class
+ })
+abstract public class DB extends RoomDatabase {
+ public static final int REVISION = 66;
+ public static final String DB_NAME = "MoLe.db";
+ public static final MutableLiveData<Boolean> initComplete = new MutableLiveData<>(false);
+ private static DB instance;
+ private static void fixTransactionDescriptionUpper(
+ @NonNull @NotNull SupportSQLiteDatabase database) {
+ try (Cursor c = database.query("SELECT id, description FROM transactions")) {
+ while (c.moveToNext()) {
+ final long id = c.getLong(0);
+ final String description = c.getString(1);
+ database.execSQL("UPDATE transactions SET description_uc=? WHERE id=?",
+ new Object[]{description.toUpperCase(), id
+ });
+ }
+ }
+ }
+ public static DB get() {
+ if (instance != null)
+ return instance;
+ synchronized (DB.class) {
+ if (instance != null)
+ return instance;
+
+ RoomDatabase.Builder<DB> builder =
+ Room.databaseBuilder(App.instance, DB.class, DB_NAME);
+ builder.addMigrations(
+ new Migration[]{singleVersionMigration(17), singleVersionMigration(18),
+ singleVersionMigration(19), singleVersionMigration(20),
+ multiVersionMigration(20, 22), multiVersionMigration(22, 30),
+ multiVersionMigration(30, 32), multiVersionMigration(32, 34),
+ multiVersionMigration(34, 40), singleVersionMigration(41),
+ multiVersionMigration(41, 58), singleVersionMigration(59),
+ singleVersionMigration(60), singleVersionMigration(61),
+ singleVersionMigration(62), singleVersionMigration(63),
+ singleVersionMigration(64), new Migration(64, 65) {
+ @Override
+ public void migrate(@NonNull @NotNull SupportSQLiteDatabase database) {
+ fixTransactionDescriptionUpper(database);
+ }
+ }, new Migration(64, 66) {
+ @Override
+ public void migrate(@NonNull @NotNull SupportSQLiteDatabase database) {
+ fixTransactionDescriptionUpper(database);
+ }
+ }, new Migration(65, 66) {
+ @Override
+ public void migrate(@NonNull @NotNull SupportSQLiteDatabase database) {
+ fixTransactionDescriptionUpper(database);
+ }
+ }
+ })
+ .addCallback(new Callback() {
+ @Override
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ super.onOpen(db);
+ db.execSQL("PRAGMA foreign_keys = ON");
+ db.execSQL("pragma case_sensitive_like" + "=ON;");
+
+ }
+ });
+
+// if (BuildConfig.DEBUG)
+// builder.setQueryCallback(((sqlQuery, bindArgs) -> Logger.debug("room", sqlQuery)),
+// Executors.newSingleThreadExecutor());
+
+ return instance = builder.build();
+ }
+ }
+ private static Migration singleVersionMigration(int toVersion) {
+ return new Migration(toVersion - 1, toVersion) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
+ String fileName = String.format(Locale.US, "db_%d", toVersion);
+
+ applyRevisionFile(db, fileName);
+
+ // when migrating to version 59, migrate profile/theme options to the
+ // SharedPreferences
+ if (toVersion == 59) {
+ try (Cursor c = db.query(
+ "SELECT p.id, p.theme FROM profiles p WHERE p.id=(SELECT o.value " +
+ "FROM options o WHERE o.profile_id=0 AND o.name=?)",
+ new Object[]{"profile_id"}))
+ {
+ if (c.moveToFirst()) {
+ long currentProfileId = c.getLong(0);
+ int currentTheme = c.getInt(1);
+
+ if (currentProfileId >= 0 && currentTheme >= 0) {
+ App.storeStartupProfileAndTheme(currentProfileId, currentTheme);
+ }
+ }
+ }
+ }
+ if (toVersion == 63) {
+ try (Cursor c = db.query("SELECT id FROM templates")) {
+ while (c.moveToNext()) {
+ db.execSQL("UPDATE templates SET uuid=? WHERE id=?",
+ new Object[]{UUID.randomUUID().toString(), c.getLong(0)});
+ }
+ }
+ }
+ }
+ };
+ }
+ private static Migration dummyVersionMigration(int toVersion) {
+ return new Migration(toVersion - 1, toVersion) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
+ Logger.debug("db",
+ String.format(Locale.ROOT, "Dummy DB migration to version %d", toVersion));
+ }
+ };
+ }
+ private static Migration multiVersionMigration(int fromVersion, int toVersion) {
+ return new Migration(fromVersion, toVersion) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase db) {
+ String fileName = String.format(Locale.US, "db_%d_%d", fromVersion, toVersion);
+
+ applyRevisionFile(db, fileName);
+ }
+ };
+ }
+ public static void applyRevisionFile(@NonNull SupportSQLiteDatabase db, String fileName) {
+ final Resources rm = App.instance.getResources();
+ int res_id = rm.getIdentifier(fileName, "raw", App.instance.getPackageName());
+ if (res_id == 0)
+ throw new SQLException(String.format(Locale.US, "No resource for %s", fileName));
+
+ try (InputStream res = rm.openRawResource(res_id)) {
+ debug("db", "Applying " + fileName);
+ InputStreamReader isr = new InputStreamReader(res);
+ BufferedReader reader = new BufferedReader(isr);
+
+ Pattern endOfStatement = Pattern.compile(";\\s*(?:--.*)?$");
+
+ String line;
+ String sqlStatement = null;
+ int lineNo = 0;
+ while ((line = reader.readLine()) != null) {
+ lineNo++;
+ if (line.startsWith("--"))
+ continue;
+ if (line.isEmpty())
+ continue;
+
+ if (sqlStatement == null)
+ sqlStatement = line;
+ else
+ sqlStatement = sqlStatement.concat(" " + line);
+
+ Matcher m = endOfStatement.matcher(line);
+ if (!m.find())
+ continue;
+
+ try {
+ db.execSQL(sqlStatement);
+ sqlStatement = null;
+ }
+ catch (Exception e) {
+ throw new RuntimeException(
+ String.format("Error applying %s, line %d, statement: %s", fileName,
+ lineNo, sqlStatement), e);
+ }
+ }
+
+ if (sqlStatement != null)
+ throw new RuntimeException(String.format(
+ "Error applying %s: EOF after continuation. Line %s, Incomplete " +
+ "statement: %s", fileName, lineNo, sqlStatement));
+
+ }
+ catch (IOException e) {
+ throw new RuntimeException(String.format("Error opening raw resource for %s", fileName),
+ e);
+ }
+ }
+ public abstract TemplateHeaderDAO getTemplateDAO();
+
+ public abstract TemplateAccountDAO getTemplateAccountDAO();
+
+ public abstract CurrencyDAO getCurrencyDAO();
+
+ public abstract AccountDAO getAccountDAO();
+
+ public abstract AccountValueDAO getAccountValueDAO();
+
+ public abstract TransactionDAO getTransactionDAO();
+
+ public abstract TransactionAccountDAO getTransactionAccountDAO();
+
+ public abstract OptionDAO getOptionDAO();
+
+ public abstract ProfileDAO getProfileDAO();
+
+ @androidx.room.Transaction
+ public void deleteAllSync() {
+ getTransactionAccountDAO().deleteAllSync();
+ getTransactionDAO().deleteAllSync();
+ getAccountValueDAO().deleteAllSync();
+ getAccountDAO().deleteAllSync();
+ getTemplateAccountDAO().deleteAllSync();
+ getTemplateDAO().deleteAllSync();
+ getCurrencyDAO().deleteAllSync();
+ getOptionDAO().deleteAllSync();
+ getProfileDAO().deleteAllSync();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+
+import org.jetbrains.annotations.NotNull;
+
+@Entity(tableName = "options", primaryKeys = {"profile_id", "name"})
+public class Option {
+ public static final String OPT_LAST_SCRAPE = "last_scrape";
+ @ColumnInfo(name = "profile_id")
+ private long profileId;
+ @NonNull
+ @ColumnInfo
+ private String name;
+ @ColumnInfo
+ private String value;
+ public Option(long profileId, @NotNull String name, String value) {
+ this.profileId = profileId;
+ this.name = name;
+ this.value = value;
+ }
+ public long getProfileId() {
+ return profileId;
+ }
+ public void setProfileId(long profileId) {
+ this.profileId = profileId;
+ }
+ @NonNull
+ public String getName() {
+ return name;
+ }
+ public void setName(@NonNull String name) {
+ this.name = name;
+ }
+ public String getValue() {
+ return value;
+ }
+ public void setValue(String value) {
+ this.value = value;
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ return getName();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+import androidx.room.Transaction;
+
+import net.ktnx.mobileledger.dao.AccountDAO;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.dao.OptionDAO;
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+@Entity(tableName = "profiles",
+ indices = {@Index(name = "profiles_uuid_idx", unique = true, value = "uuid")})
+public class Profile {
+ public static final long NO_PROFILE_ID = 0;
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @NonNull
+ @ColumnInfo
+ private String name = "";
+ @NonNull
+ @ColumnInfo()
+ private String uuid;
+ @NonNull
+ @ColumnInfo
+ private String url = "";
+ @ColumnInfo(name = "use_authentication")
+ private boolean useAuthentication;
+ @ColumnInfo(name = "auth_user")
+ private String authUser;
+ @ColumnInfo(name = "auth_password")
+ private String authPassword;
+ @ColumnInfo(name = "order_no")
+ private int orderNo;
+ @ColumnInfo(name = "permit_posting")
+ private boolean permitPosting;
+ @ColumnInfo(defaultValue = "-1")
+ private int theme = -1;
+ @ColumnInfo(name = "preferred_accounts_filter")
+ private String preferredAccountsFilter;
+ @ColumnInfo(name = "future_dates")
+ private int futureDates;
+ @ColumnInfo(name = "api_version")
+ private int apiVersion;
+ @ColumnInfo(name = "show_commodity_by_default")
+ private boolean showCommodityByDefault;
+ @ColumnInfo(name = "default_commodity")
+ private String defaultCommodity;
+ @ColumnInfo(name = "show_comments_by_default", defaultValue = "1")
+ private boolean showCommentsByDefault = true;
+ @ColumnInfo(name = "detected_version_pre_1_19")
+ private boolean detectedVersionPre_1_19;
+ @ColumnInfo(name = "detected_version_major")
+ private int detectedVersionMajor;
+ @ColumnInfo(name = "detected_version_minor")
+ private int detectedVersionMinor;
+ public Profile() {
+ uuid = UUID.randomUUID()
+ .toString();
+ }
+ public String getUuid() {
+ return uuid;
+ }
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ @NonNull
+ public String getName() {
+ return name;
+ }
+ public void setName(@NonNull String name) {
+ this.name = name;
+ }
+ @NonNull
+ public String getUrl() {
+ return url;
+ }
+ public void setUrl(@NonNull String url) {
+ this.url = url;
+ }
+ public boolean useAuthentication() {
+ return useAuthentication;
+ }
+ public void setUseAuthentication(boolean useAuthentication) {
+ this.useAuthentication = useAuthentication;
+ }
+ public String getAuthUser() {
+ return authUser;
+ }
+ public void setAuthUser(String authUser) {
+ this.authUser = authUser;
+ }
+ public String getAuthPassword() {
+ return authPassword;
+ }
+ public void setAuthPassword(String authPassword) {
+ this.authPassword = authPassword;
+ }
+ public int getOrderNo() {
+ return orderNo;
+ }
+ public void setOrderNo(int orderNo) {
+ this.orderNo = orderNo;
+ }
+ public boolean permitPosting() {
+ return permitPosting;
+ }
+ public void setPermitPosting(boolean permitPosting) {
+ this.permitPosting = permitPosting;
+ }
+ public int getTheme() {
+ return theme;
+ }
+ public void setTheme(int theme) {
+ this.theme = theme;
+ }
+ public String getPreferredAccountsFilter() {
+ return preferredAccountsFilter;
+ }
+ public void setPreferredAccountsFilter(String preferredAccountsFilter) {
+ this.preferredAccountsFilter = preferredAccountsFilter;
+ }
+ public int getFutureDates() {
+ return futureDates;
+ }
+ public void setFutureDates(int futureDates) {
+ this.futureDates = futureDates;
+ }
+ public int getApiVersion() {
+ return apiVersion;
+ }
+ public void setApiVersion(int apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+ public boolean getShowCommodityByDefault() {
+ return showCommodityByDefault;
+ }
+ public void setShowCommodityByDefault(boolean showCommodityByDefault) {
+ this.showCommodityByDefault = showCommodityByDefault;
+ }
+ @NotNull
+ public String getDefaultCommodity() {
+ return defaultCommodity;
+ }
+ public void setDefaultCommodity(@org.jetbrains.annotations.Nullable String defaultCommodity) {
+ this.defaultCommodity = Misc.nullIsEmpty(defaultCommodity);
+ }
+ public boolean getShowCommentsByDefault() {
+ return showCommentsByDefault;
+ }
+ public void setShowCommentsByDefault(boolean showCommentsByDefault) {
+ this.showCommentsByDefault = showCommentsByDefault;
+ }
+ public boolean detectedVersionPre_1_19() {
+ return detectedVersionPre_1_19;
+ }
+ public void setDetectedVersionPre_1_19(boolean detectedVersionPre_1_19) {
+ this.detectedVersionPre_1_19 = detectedVersionPre_1_19;
+ }
+ public int getDetectedVersionMajor() {
+ return detectedVersionMajor;
+ }
+ public void setDetectedVersionMajor(int detectedVersionMajor) {
+ this.detectedVersionMajor = detectedVersionMajor;
+ }
+ public int getDetectedVersionMinor() {
+ return detectedVersionMinor;
+ }
+ public void setDetectedVersionMinor(int detectedVersionMinor) {
+ this.detectedVersionMinor = detectedVersionMinor;
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ return getName();
+ }
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (!(o instanceof Profile))
+ return false;
+ Profile p = (Profile) o;
+ return id == p.id && Misc.equalStrings(name, p.name) && Misc.equalStrings(uuid, p.uuid) &&
+ Misc.equalStrings(url, p.url) && useAuthentication == p.useAuthentication &&
+ Misc.equalStrings(authUser, p.authUser) &&
+ Misc.equalStrings(authPassword, p.authPassword) && orderNo == p.orderNo &&
+ permitPosting == p.permitPosting && theme == p.theme &&
+ Misc.equalStrings(preferredAccountsFilter, p.preferredAccountsFilter) &&
+ futureDates == p.futureDates && apiVersion == p.apiVersion &&
+ showCommentsByDefault == p.showCommentsByDefault &&
+ Misc.equalStrings(defaultCommodity, p.defaultCommodity) &&
+ showCommentsByDefault == p.showCommentsByDefault &&
+ detectedVersionPre_1_19 == p.detectedVersionPre_1_19 &&
+ detectedVersionMajor == p.detectedVersionMajor &&
+ detectedVersionMinor == p.detectedVersionMinor;
+ }
+ @Transaction
+ public void wipeAllDataSync() {
+ OptionDAO optDao = DB.get()
+ .getOptionDAO();
+ optDao.deleteSync(optDao.allForProfileSync(id));
+
+ AccountDAO accDao = DB.get()
+ .getAccountDAO();
+ accDao.deleteSync(accDao.allForProfileSync(id));
+
+ TransactionDAO trnDao = DB.get()
+ .getTransactionDAO();
+ trnDao.deleteSync(trnDao.getAllForProfileUnorderedSync(id));
+ }
+ public void wipeAllData() {
+ BaseDAO.runAsync(this::wipeAllDataSync);
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import org.jetbrains.annotations.NotNull;
+
+@Entity(tableName = "template_accounts",
+ indices = {@Index(name = "fk_template_accounts_template", value = "template_id"),
+ @Index(name = "fk_template_accounts_currency", value = "currency")
+ }, foreignKeys = {@ForeignKey(childColumns = "template_id", parentColumns = "id",
+ entity = TemplateHeader.class, onDelete = ForeignKey.CASCADE,
+ onUpdate = ForeignKey.RESTRICT),
+ @ForeignKey(childColumns = "currency", parentColumns = "id",
+ entity = Currency.class, onDelete = ForeignKey.RESTRICT,
+ onUpdate = ForeignKey.RESTRICT)
+})
+public class TemplateAccount extends TemplateBase {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @ColumnInfo(name = "template_id")
+ private long templateId;
+ @ColumnInfo(name = "acc")
+ private String accountName;
+ @ColumnInfo(name = "position")
+ @NonNull
+ private Long position;
+ @ColumnInfo(name = "acc_match_group")
+ private Integer accountNameMatchGroup;
+ @ColumnInfo
+ private Long currency;
+ @ColumnInfo(name = "currency_match_group")
+ private Integer currencyMatchGroup;
+ @ColumnInfo(name = "amount")
+ private Float amount;
+ @ColumnInfo(name = "amount_match_group")
+ private Integer amountMatchGroup;
+ @ColumnInfo(name = "comment")
+ private String accountComment;
+ @ColumnInfo(name = "comment_match_group")
+ private Integer accountCommentMatchGroup;
+ @ColumnInfo(name = "negate_amount")
+ private Boolean negateAmount;
+ public TemplateAccount(@NotNull Long id, @NonNull Long templateId, @NonNull Long position) {
+ this.id = id;
+ this.templateId = templateId;
+ this.position = position;
+ }
+ public TemplateAccount(TemplateAccount o) {
+ id = o.id;
+ templateId = o.templateId;
+ accountName = o.accountName;
+ position = o.position;
+ accountNameMatchGroup = o.accountNameMatchGroup;
+ currency = o.currency;
+ currencyMatchGroup = o.currencyMatchGroup;
+ amount = o.amount;
+ amountMatchGroup = o.amountMatchGroup;
+ accountComment = o.accountComment;
+ accountCommentMatchGroup = o.accountCommentMatchGroup;
+ negateAmount = o.negateAmount;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ public Boolean getNegateAmount() {
+ return negateAmount;
+ }
+ public void setNegateAmount(Boolean negateAmount) {
+ this.negateAmount = negateAmount;
+ }
+ public long getTemplateId() {
+ return templateId;
+ }
+ public void setTemplateId(long templateId) {
+ this.templateId = templateId;
+ }
+ @NonNull
+ public String getAccountName() {
+ return accountName;
+ }
+ public void setAccountName(@NonNull String accountName) {
+ this.accountName = accountName;
+ }
+ @NonNull
+ public Long getPosition() {
+ return position;
+ }
+ public void setPosition(@NonNull Long position) {
+ this.position = position;
+ }
+ public void setPosition(int position) {
+ this.position = (long) position;
+ }
+ public Integer getAccountNameMatchGroup() {
+ return accountNameMatchGroup;
+ }
+ public void setAccountNameMatchGroup(Integer accountNameMatchGroup) {
+ this.accountNameMatchGroup = accountNameMatchGroup;
+ }
+ public Long getCurrency() {
+ return currency;
+ }
+ public void setCurrency(Long currency) {
+ this.currency = currency;
+ }
+ public Currency getCurrencyObject() {
+ if (currency == null || currency <= 0)
+ return null;
+ return DB.get()
+ .getCurrencyDAO()
+ .getByIdSync(currency);
+ }
+ public Integer getCurrencyMatchGroup() {
+ return currencyMatchGroup;
+ }
+ public void setCurrencyMatchGroup(Integer currencyMatchGroup) {
+ this.currencyMatchGroup = currencyMatchGroup;
+ }
+ public Float getAmount() {
+ return amount;
+ }
+ public void setAmount(Float amount) {
+ this.amount = amount;
+ }
+ public Integer getAmountMatchGroup() {
+ return amountMatchGroup;
+ }
+ public void setAmountMatchGroup(Integer amountMatchGroup) {
+ this.amountMatchGroup = amountMatchGroup;
+ }
+ public String getAccountComment() {
+ return accountComment;
+ }
+ public void setAccountComment(String accountComment) {
+ this.accountComment = accountComment;
+ }
+ public Integer getAccountCommentMatchGroup() {
+ return accountCommentMatchGroup;
+ }
+ public void setAccountCommentMatchGroup(Integer accountCommentMatchGroup) {
+ this.accountCommentMatchGroup = accountCommentMatchGroup;
+ }
+ public TemplateAccount createDuplicate(TemplateHeader header) {
+ TemplateAccount dup = new TemplateAccount(this);
+ dup.id = 0;
+ dup.templateId = header.getId();
+
+ return dup;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+public class TemplateBase {}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+@Entity(tableName = "templates",
+ indices = {@Index(name = "templates_uuid_idx", unique = true, value = "uuid")})
+public class TemplateHeader extends TemplateBase {
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @ColumnInfo(name = "name")
+ @NonNull
+ private String name;
+ @NonNull
+ @ColumnInfo
+ private String uuid;
+ @NonNull
+ @ColumnInfo(name = "regular_expression")
+ private String regularExpression;
+ @ColumnInfo(name = "test_text")
+ private String testText;
+ @ColumnInfo(name = "transaction_description")
+ private String transactionDescription;
+ @ColumnInfo(name = "transaction_description_match_group")
+ private Integer transactionDescriptionMatchGroup;
+ @ColumnInfo(name = "transaction_comment")
+ private String transactionComment;
+ @ColumnInfo(name = "transaction_comment_match_group")
+ private Integer transactionCommentMatchGroup;
+ @ColumnInfo(name = "date_year")
+ private Integer dateYear;
+ @ColumnInfo(name = "date_year_match_group")
+ private Integer dateYearMatchGroup;
+ @ColumnInfo(name = "date_month")
+ private Integer dateMonth;
+ @ColumnInfo(name = "date_month_match_group")
+ private Integer dateMonthMatchGroup;
+ @ColumnInfo(name = "date_day")
+ private Integer dateDay;
+ @ColumnInfo(name = "date_day_match_group")
+ private Integer dateDayMatchGroup;
+ @ColumnInfo(name = "is_fallback")
+ private boolean isFallback;
+ public TemplateHeader(@NotNull Long id, @NonNull String name,
+ @NonNull String regularExpression) {
+ this.id = id;
+ this.name = name;
+ this.regularExpression = regularExpression;
+ this.uuid = UUID.randomUUID()
+ .toString();
+ }
+ public TemplateHeader(TemplateHeader origin) {
+ id = origin.id;
+ name = origin.name;
+ uuid = origin.uuid;
+ regularExpression = origin.regularExpression;
+ testText = origin.testText;
+ transactionDescription = origin.transactionDescription;
+ transactionDescriptionMatchGroup = origin.transactionDescriptionMatchGroup;
+ transactionComment = origin.transactionComment;
+ transactionCommentMatchGroup = origin.transactionCommentMatchGroup;
+ dateYear = origin.dateYear;
+ dateYearMatchGroup = origin.dateYearMatchGroup;
+ dateMonth = origin.dateMonth;
+ dateMonthMatchGroup = origin.dateMonthMatchGroup;
+ dateDay = origin.dateDay;
+ dateDayMatchGroup = origin.dateDayMatchGroup;
+ isFallback = origin.isFallback;
+ }
+ @NonNull
+ public String getUuid() {
+ return uuid;
+ }
+ public void setUuid(@NonNull String uuid) {
+ this.uuid = uuid;
+ }
+ public boolean isFallback() {
+ return isFallback;
+ }
+ public void setFallback(boolean fallback) {
+ isFallback = fallback;
+ }
+ public String getTestText() {
+ return testText;
+ }
+ public void setTestText(String testText) {
+ this.testText = testText;
+ }
+ public Integer getTransactionDescriptionMatchGroup() {
+ return transactionDescriptionMatchGroup;
+ }
+ public void setTransactionDescriptionMatchGroup(Integer transactionDescriptionMatchGroup) {
+ this.transactionDescriptionMatchGroup = transactionDescriptionMatchGroup;
+ }
+ public Integer getTransactionCommentMatchGroup() {
+ return transactionCommentMatchGroup;
+ }
+ public void setTransactionCommentMatchGroup(Integer transactionCommentMatchGroup) {
+ this.transactionCommentMatchGroup = transactionCommentMatchGroup;
+ }
+ public Integer getDateYear() {
+ return dateYear;
+ }
+ public void setDateYear(Integer dateYear) {
+ this.dateYear = dateYear;
+ }
+ public Integer getDateMonth() {
+ return dateMonth;
+ }
+ public void setDateMonth(Integer dateMonth) {
+ this.dateMonth = dateMonth;
+ }
+ public Integer getDateDay() {
+ return dateDay;
+ }
+ public void setDateDay(Integer dateDay) {
+ this.dateDay = dateDay;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ @NonNull
+ public String getName() {
+ return name;
+ }
+ public void setName(@NonNull String name) {
+ this.name = name;
+ }
+ @NonNull
+ public String getRegularExpression() {
+ return regularExpression;
+ }
+ public void setRegularExpression(@NonNull String regularExpression) {
+ this.regularExpression = regularExpression;
+ }
+ public String getTransactionDescription() {
+ return transactionDescription;
+ }
+ public void setTransactionDescription(String transactionDescription) {
+ this.transactionDescription = transactionDescription;
+ }
+ public String getTransactionComment() {
+ return transactionComment;
+ }
+ public void setTransactionComment(String transactionComment) {
+ this.transactionComment = transactionComment;
+ }
+ public Integer getDateYearMatchGroup() {
+ return dateYearMatchGroup;
+ }
+ public void setDateYearMatchGroup(Integer dateYearMatchGroup) {
+ this.dateYearMatchGroup = dateYearMatchGroup;
+ }
+ public Integer getDateMonthMatchGroup() {
+ return dateMonthMatchGroup;
+ }
+ public void setDateMonthMatchGroup(Integer dateMonthMatchGroup) {
+ this.dateMonthMatchGroup = dateMonthMatchGroup;
+ }
+ public Integer getDateDayMatchGroup() {
+ return dateDayMatchGroup;
+ }
+ public void setDateDayMatchGroup(Integer dateDayMatchGroup) {
+ this.dateDayMatchGroup = dateDayMatchGroup;
+ }
+ @Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj == null)
+ return false;
+ if (!(obj instanceof TemplateHeader))
+ return false;
+
+ TemplateHeader o = (TemplateHeader) obj;
+
+ return Misc.equalLongs(id, o.id) && Misc.equalStrings(name, o.name) &&
+ Misc.equalStrings(regularExpression, o.regularExpression) &&
+ Misc.equalStrings(transactionDescription, o.transactionDescription) &&
+ Misc.equalStrings(transactionComment, o.transactionComment) &&
+ Misc.equalIntegers(transactionDescriptionMatchGroup,
+ o.transactionDescriptionMatchGroup) &&
+ Misc.equalIntegers(transactionCommentMatchGroup, o.transactionCommentMatchGroup) &&
+ Misc.equalIntegers(dateDay, o.dateDay) &&
+ Misc.equalIntegers(dateDayMatchGroup, o.dateDayMatchGroup) &&
+ Misc.equalIntegers(dateMonth, o.dateMonth) &&
+ Misc.equalIntegers(dateMonthMatchGroup, o.dateMonthMatchGroup) &&
+ Misc.equalIntegers(dateYear, o.dateYear) &&
+ Misc.equalIntegers(dateYearMatchGroup, o.dateYearMatchGroup);
+ }
+ public TemplateHeader createDuplicate() {
+ TemplateHeader dup = new TemplateHeader(this);
+ dup.id = 0;
+ dup.uuid = UUID.randomUUID()
+ .toString();
+
+ return dup;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TemplateWithAccounts {
+ @Embedded
+ public TemplateHeader header;
+ @Relation(parentColumn = "id", entityColumn = "template_id")
+ public List<TemplateAccount> accounts;
+
+ public static TemplateWithAccounts from(TemplateWithAccounts o) {
+ TemplateWithAccounts result = new TemplateWithAccounts();
+ result.header = new TemplateHeader(o.header);
+ result.accounts = new ArrayList<>();
+ for (TemplateAccount acc : o.accounts) {
+ result.accounts.add(new TemplateAccount(acc));
+ }
+
+ return result;
+ }
+ public Long getId() {
+ return header.getId();
+ }
+ public TemplateWithAccounts createDuplicate() {
+ TemplateWithAccounts result = new TemplateWithAccounts();
+ result.header = header.createDuplicate();
+ result.accounts = new ArrayList<>();
+ for (TemplateAccount acc : accounts) {
+ result.accounts.add(acc.createDuplicate(result.header));
+ }
+
+ return result;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import org.jetbrains.annotations.NotNull;
+
+/*
+create table transactions(profile varchar not null, id integer not null, data_hash varchar not
+null, year integer not null, month integer not null, day integer not null, description varchar
+collate NOCASE not null, comment varchar, generation integer default 0, primary key(profile,id));
+create unique index un_transactions_data_hash on transactions(profile,data_hash);
+create index idx_transaction_description on transactions(description);
+ */
+@Entity(tableName = "transactions", foreignKeys = {
+ @ForeignKey(entity = Profile.class, parentColumns = "id", childColumns = "profile_id",
+ onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.RESTRICT)
+}, indices = {@Index(name = "un_transactions_ledger_id", unique = true,
+ value = {"profile_id", "ledger_id"}),
+ @Index(name = "idx_transaction_description", value = "description"),
+ @Index(name = "fk_transaction_profile", value = "profile_id")
+})
+public class Transaction {
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ long id;
+ @ColumnInfo(name = "ledger_id")
+ long ledgerId;
+ @ColumnInfo(name = "profile_id")
+ private long profileId;
+ @ColumnInfo(name = "data_hash")
+ @NonNull
+ private String dataHash;
+ @ColumnInfo
+ private int year;
+ @ColumnInfo
+ private int month;
+ @ColumnInfo
+ private int day;
+ @ColumnInfo(collate = ColumnInfo.NOCASE)
+ @NonNull
+ private String description;
+ @ColumnInfo(name = "description_uc")
+ @NonNull
+ private String descriptionUpper;
+ @ColumnInfo
+ private String comment;
+ @ColumnInfo
+ private long generation = 0;
+ @NonNull
+ public String getDescriptionUpper() {
+ return descriptionUpper;
+ }
+ public void setDescriptionUpper(@NonNull String descriptionUpper) {
+ this.descriptionUpper = descriptionUpper;
+ }
+ public long getLedgerId() {
+ return ledgerId;
+ }
+ public void setLedgerId(long ledgerId) {
+ this.ledgerId = ledgerId;
+ }
+ public long getProfileId() {
+ return profileId;
+ }
+ public void setProfileId(long profileId) {
+ this.profileId = profileId;
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ public String getDataHash() {
+ return dataHash;
+ }
+ public void setDataHash(@NotNull String dataHash) {
+ this.dataHash = dataHash;
+ }
+ public int getYear() {
+ return year;
+ }
+ public void setYear(int year) {
+ this.year = year;
+ }
+ public int getMonth() {
+ return month;
+ }
+ public void setMonth(int month) {
+ this.month = month;
+ }
+ public int getDay() {
+ return day;
+ }
+ public void setDay(int day) {
+ this.day = day;
+ }
+ public String getDescription() {
+ return description;
+ }
+ public void setDescription(String description) {
+ this.description = description;
+ setDescriptionUpper(description.toUpperCase());
+ }
+ public String getComment() {
+ return comment;
+ }
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+ public long getGeneration() {
+ return generation;
+ }
+ public void setGeneration(long generation) {
+ this.generation = generation;
+ }
+
+ public void copyDataFrom(Transaction o) {
+ // id = o.id;
+ ledgerId = o.ledgerId;
+ profileId = o.profileId;
+ dataHash = o.dataHash;
+ year = o.year;
+ month = o.month;
+ day = o.day;
+ description = o.description;
+ descriptionUpper = o.description.toUpperCase();
+ comment = o.comment;
+ generation = o.generation;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.annotation.NonNull;
+import androidx.room.ColumnInfo;
+import androidx.room.Entity;
+import androidx.room.ForeignKey;
+import androidx.room.Index;
+import androidx.room.PrimaryKey;
+
+import net.ktnx.mobileledger.utils.Misc;
+
+@Entity(tableName = "transaction_accounts", foreignKeys = {
+ @ForeignKey(entity = Transaction.class, parentColumns = {"id"},
+ childColumns = {"transaction_id"}, onDelete = ForeignKey.CASCADE,
+ onUpdate = ForeignKey.RESTRICT)
+}, indices = {@Index(name = "fk_trans_acc_trans", value = {"transaction_id"}),
+ @Index(name = "un_transaction_accounts", unique = true,
+ value = {"transaction_id", "order_no"})
+})
+public class TransactionAccount {
+ @ColumnInfo
+ @PrimaryKey(autoGenerate = true)
+ private long id;
+ @ColumnInfo(name = "transaction_id")
+ private long transactionId;
+ @ColumnInfo(name = "order_no")
+ private int orderNo;
+ @ColumnInfo(name = "account_name")
+ @NonNull
+ private String accountName;
+ @ColumnInfo(defaultValue = "")
+ @NonNull
+ private String currency = "";
+ @ColumnInfo
+ private float amount;
+ @ColumnInfo
+ private String comment;
+ @ColumnInfo(defaultValue = "0")
+ private long generation = 0;
+ public long getId() {
+ return id;
+ }
+ public void setId(long id) {
+ this.id = id;
+ }
+ @NonNull
+ public long getTransactionId() {
+ return transactionId;
+ }
+ public void setTransactionId(long transactionId) {
+ this.transactionId = transactionId;
+ }
+ public int getOrderNo() {
+ return orderNo;
+ }
+ public void setOrderNo(int orderNo) {
+ this.orderNo = orderNo;
+ }
+ @NonNull
+ public String getAccountName() {
+ return accountName;
+ }
+ public void setAccountName(@NonNull String accountName) {
+ this.accountName = accountName;
+ }
+ @NonNull
+ public String getCurrency() {
+ return currency;
+ }
+ public void setCurrency(@NonNull String currency) {
+ this.currency = currency;
+ }
+ public float getAmount() {
+ return amount;
+ }
+ public void setAmount(float amount) {
+ this.amount = amount;
+ }
+ public String getComment() {
+ return comment;
+ }
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+ public long getGeneration() {
+ return generation;
+ }
+ public void setGeneration(long generation) {
+ this.generation = generation;
+ }
+
+ public void copyDataFrom(TransactionAccount o) {
+ // id = o.id
+ transactionId = o.transactionId;
+ orderNo = o.orderNo;
+ accountName = o.accountName;
+ currency = Misc.nullIsEmpty(o.currency);
+ amount = o.amount;
+ comment = o.comment;
+ generation = o.generation;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import android.content.Context;
+import android.widget.ArrayAdapter;
+import android.widget.Filter;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransactionDescriptionAutocompleteAdapter extends ArrayAdapter<String> {
+ private final TransactionFilter filter = new TransactionFilter();
+ private final TransactionDAO dao = DB.get()
+ .getTransactionDAO();
+ public TransactionDescriptionAutocompleteAdapter(Context context) {
+ super(context, android.R.layout.simple_dropdown_item_1line, new ArrayList<>());
+ }
+ @NonNull
+ @Override
+ public Filter getFilter() {
+ return filter;
+ }
+ class TransactionFilter extends Filter {
+ @Override
+ protected FilterResults performFiltering(CharSequence constraint) {
+ FilterResults results = new FilterResults();
+ if (constraint == null) {
+ results.count = 0;
+ return results;
+ }
+
+ Logger.debug("acc", String.format("Looking for description '%s'", constraint));
+ final List<String> matches = TransactionDAO.unbox(dao.lookupDescriptionSync(
+ String.valueOf(constraint)
+ .toUpperCase()));
+ results.values = matches;
+ results.count = matches.size();
+
+ return results;
+ }
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void publishResults(CharSequence constraint, FilterResults results) {
+ if (results.values == null) {
+ notifyDataSetInvalidated();
+ }
+ else {
+ setNotifyOnChange(false);
+ clear();
+ addAll((List<String>) results.values);
+ notifyDataSetChanged();
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.db;
+
+import androidx.room.Embedded;
+import androidx.room.Relation;
+
+import java.util.List;
+
+public class TransactionWithAccounts {
+ @Embedded
+ public Transaction transaction;
+ @Relation(parentColumn = "id", entityColumn = "transaction_id")
+ public List<TransactionAccount> accounts;
+}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.err;
-public class HTTPException extends Throwable {
+public class HTTPException extends Exception {
private final int responseCode;
- private final String responseMessage;
public int getResponseCode() {
return responseCode;
}
- public String getResponseMessage() {
- return responseMessage;
- }
public HTTPException(int responseCode, String responseMessage) {
+ super(responseMessage);
this.responseCode = responseCode;
- this.responseMessage = responseMessage;
}
}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import android.content.res.Resources;
+import android.util.SparseArray;
+
+import net.ktnx.mobileledger.R;
+
+public enum API {
+ auto(0), html(-1), v1_14(-2), v1_15(-3), v1_19_1(-4), v1_23(-5);
+ private static final SparseArray<API> map = new SparseArray<>();
+ public static API[] allVersions = {v1_23, v1_19_1, v1_15, v1_14};
+
+ static {
+ for (API item : API.values()) {
+ map.put(item.value, item);
+ }
+ }
+
+ private final int value;
+
+ API(int value) {
+ this.value = value;
+ }
+ public static API valueOf(int i) {
+ return map.get(i, auto);
+ }
+ public int toInt() {
+ return this.value;
+ }
+ public String getDescription(Resources resources) {
+ switch (this) {
+ case auto:
+ return resources.getString(R.string.api_auto);
+ case html:
+ return resources.getString(R.string.api_html);
+ case v1_14:
+ return resources.getString(R.string.api_1_14);
+ case v1_15:
+ return resources.getString(R.string.api_1_15);
+ case v1_19_1:
+ return resources.getString(R.string.api_1_19_1);
+ case v1_23:
+ return resources.getString(R.string.api_1_23);
+ default:
+ throw new IllegalStateException("Unexpected value: " + value);
+ }
+ }
+ public String getDescription() {
+ switch (this) {
+ case auto:
+ return "(automatic)";
+ case html:
+ return "(HTML)";
+ case v1_14:
+ return "1.14";
+ case v1_15:
+ return "1.15";
+ case v1_19_1:
+ return "1.19.1";
+ case v1_23:
+ return "1.23";
+ default:
+ throw new IllegalStateException("Unexpected value: " + this);
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.model.LedgerAccount;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+abstract public class AccountListParser {
+ protected MappingIterator<net.ktnx.mobileledger.json.ParsedLedgerAccount> iterator;
+ public static AccountListParser forApiVersion(API version, InputStream input)
+ throws IOException {
+ switch (version) {
+ case v1_14:
+ return new net.ktnx.mobileledger.json.v1_14.AccountListParser(input);
+ case v1_15:
+ return new net.ktnx.mobileledger.json.v1_15.AccountListParser(input);
+ case v1_19_1:
+ return new net.ktnx.mobileledger.json.v1_19_1.AccountListParser(input);
+ case v1_23:
+ return new net.ktnx.mobileledger.json.v1_23.AccountListParser(input);
+ default:
+ throw new RuntimeException("Unsupported version " + version.toString());
+ }
+
+ }
+ public abstract API getApiVersion();
+ public LedgerAccount nextAccount(RetrieveTransactionsTask task,
+ HashMap<String, LedgerAccount> map) {
+ if (!iterator.hasNext())
+ return null;
+
+ LedgerAccount next = iterator.next()
+ .toLedgerAccount(task, map);
+
+ if (next.getName()
+ .equalsIgnoreCase("root"))
+ return nextAccount(task, map);
+
+ debug("accounts", String.format("Got account '%s' [%s]", next.getName(),
+ getApiVersion().getDescription()));
+ return next;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import androidx.annotation.Nullable;
+
+public class ApiNotSupportedException extends Throwable {
+ public ApiNotSupportedException() {
+ }
+ public ApiNotSupportedException(@Nullable String message) {
+ super(message);
+ }
+ public ApiNotSupportedException(@Nullable String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+ public ApiNotSupportedException(@Nullable Throwable cause) {
+ super(cause);
+ }
+ public ApiNotSupportedException(@Nullable String message, @Nullable Throwable cause,
+ boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+abstract public class Gateway {
+ public static Gateway forApiVersion(API apiVersion) {
+ switch (apiVersion) {
+ case v1_14:
+ return new net.ktnx.mobileledger.json.v1_14.Gateway();
+ case v1_15:
+ return new net.ktnx.mobileledger.json.v1_15.Gateway();
+ case v1_19_1:
+ return new net.ktnx.mobileledger.json.v1_19_1.Gateway();
+ case v1_23:
+ return new net.ktnx.mobileledger.json.v1_23.Gateway();
+ default:
+ throw new RuntimeException(
+ "JSON API version " + apiVersion + " save implementation missing");
+ }
+ }
+ public abstract String transactionSaveRequest(LedgerTransaction ledgerTransaction)
+ throws JsonProcessingException;
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import androidx.annotation.NonNull;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedBalance {
+ private ParsedQuantity aquantity;
+ private String acommodity;
+ public ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ @NonNull
+ public String getAcommodity() {
+ return (acommodity == null) ? "" : acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.model.LedgerAccount;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+public abstract class ParsedLedgerAccount {
+ private String aname;
+ private int anumpostings;
+ public abstract List<SimpleBalance> getSimpleBalance();
+ public String getAname() {
+ return aname;
+ }
+ public void setAname(String aname) {
+ this.aname = aname;
+ }
+ public int getAnumpostings() {
+ return anumpostings;
+ }
+ public void setAnumpostings(int anumpostings) {
+ this.anumpostings = anumpostings;
+ }
+ public LedgerAccount toLedgerAccount(RetrieveTransactionsTask task,
+ HashMap<String, LedgerAccount> map) {
+ task.addNumberOfPostings(getAnumpostings());
+ final String accName = getAname();
+ LedgerAccount acc = map.get(accName);
+ if (acc != null)
+ throw new RuntimeException(
+ String.format("Account '%s' already present", acc.getName()));
+ String parentName = LedgerAccount.extractParentName(accName);
+ ArrayList<LedgerAccount> createdParents = new ArrayList<>();
+ LedgerAccount parent;
+ if (parentName == null) {
+ parent = null;
+ }
+ else {
+ parent = task.ensureAccountExists(parentName, map, createdParents);
+ parent.setHasSubAccounts(true);
+ }
+ acc = new LedgerAccount(accName, parent);
+ map.put(accName, acc);
+
+ String lastCurrency = null;
+ float lastCurrencyAmount = 0;
+ for (SimpleBalance b : getSimpleBalance()) {
+ task.throwIfCancelled();
+ final String currency = b.getCommodity();
+ final float amount = b.getAmount();
+ if (currency.equals(lastCurrency)) {
+ lastCurrencyAmount += amount;
+ }
+ else {
+ if (lastCurrency != null) {
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
+ }
+ lastCurrency = currency;
+ lastCurrencyAmount = amount;
+ }
+ }
+ if (lastCurrency != null) {
+ acc.addAmount(lastCurrencyAmount, lastCurrency);
+ }
+ for (LedgerAccount p : createdParents)
+ acc.propagateAmountsTo(p);
+
+ return acc;
+ }
+
+ static public class SimpleBalance {
+ private String commodity;
+ private float amount;
+ public SimpleBalance(String commodity, float amount) {
+ this.commodity = commodity;
+ this.amount = amount;
+ }
+ public String getCommodity() {
+ return commodity;
+ }
+ public void setCommodity(String commodity) {
+ this.commodity = commodity;
+ }
+ public float getAmount() {
+ return amount;
+ }
+ public void setAmount(float amount) {
+ this.amount = amount;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import net.ktnx.mobileledger.json.v1_15.ParsedQuantity;
+import net.ktnx.mobileledger.json.v1_15.ParsedStyle;
+
+public class ParsedPrice {
+ private String tag;
+ private Contents contents;
+ public ParsedPrice() {
+ tag = "NoPrice";
+ }
+ public Contents getContents() {
+ return contents;
+ }
+ public void setContents(Contents contents) {
+ this.contents = contents;
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+ private static class Contents {
+ private ParsedPrice aprice;
+ private net.ktnx.mobileledger.json.v1_15.ParsedQuantity aquantity;
+ private String acommodity;
+ private boolean aismultiplier;
+ private net.ktnx.mobileledger.json.v1_15.ParsedStyle astyle;
+ public Contents() {
+ acommodity = "";
+ }
+ public ParsedPrice getAprice() {
+ return aprice;
+ }
+ public void setAprice(ParsedPrice aprice) {
+ this.aprice = aprice;
+ }
+ public net.ktnx.mobileledger.json.v1_15.ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ public String getAcommodity() {
+ return acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+ public boolean isAismultiplier() {
+ return aismultiplier;
+ }
+ public void setAismultiplier(boolean aismultiplier) {
+ this.aismultiplier = aismultiplier;
+ }
+ public net.ktnx.mobileledger.json.v1_15.ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedQuantity {
+ private long decimalMantissa;
+ private int decimalPlaces;
+ public ParsedQuantity() {
+ }
+ public ParsedQuantity(String input) {
+ parseString(input);
+ }
+ public long getDecimalMantissa() {
+ return decimalMantissa;
+ }
+ public void setDecimalMantissa(long decimalMantissa) {
+ this.decimalMantissa = decimalMantissa;
+ }
+ public int getDecimalPlaces() {
+ return decimalPlaces;
+ }
+ public void setDecimalPlaces(int decimalPlaces) {
+ this.decimalPlaces = decimalPlaces;
+ }
+ public float asFloat() {
+ return (float) (decimalMantissa * Math.pow(10, -decimalPlaces));
+ }
+ public void parseString(String input) {
+ int pointPos = input.indexOf('.');
+ if (pointPos >= 0) {
+ String integral = input.replace(".", "");
+ decimalMantissa = Long.parseLong(integral);
+ decimalPlaces = input.length() - pointPos - 1;
+ }
+ else {
+ decimalMantissa = Long.parseLong(input);
+ decimalPlaces = 0;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedStyle {
+ private char asdecimalpoint;
+ private char ascommodityside;
+ private int digitgroups;
+ private boolean ascommodityspaced;
+ public ParsedStyle() {
+ }
+ public char getAsdecimalpoint() {
+ return asdecimalpoint;
+ }
+ public void setAsdecimalpoint(char asdecimalpoint) {
+ this.asdecimalpoint = asdecimalpoint;
+ }
+ public char getAscommodityside() {
+ return ascommodityside;
+ }
+ public void setAscommodityside(char ascommodityside) {
+ this.ascommodityside = ascommodityside;
+ }
+ public int getDigitgroups() {
+ return digitgroups;
+ }
+ public void setDigitgroups(int digitgroups) {
+ this.digitgroups = digitgroups;
+ }
+ public boolean isAscommodityspaced() {
+ return ascommodityspaced;
+ }
+ public void setAscommodityspaced(boolean ascommodityspaced) {
+ this.ascommodityspaced = ascommodityspaced;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+
+public abstract class TransactionListParser {
+ public static TransactionListParser forApiVersion(API apiVersion, InputStream input)
+ throws IOException {
+ switch (apiVersion) {
+ case v1_14:
+ return new net.ktnx.mobileledger.json.v1_14.TransactionListParser(input);
+ case v1_15:
+ return new net.ktnx.mobileledger.json.v1_15.TransactionListParser(input);
+ case v1_19_1:
+ return new net.ktnx.mobileledger.json.v1_19_1.TransactionListParser(input);
+ case v1_23:
+ return new net.ktnx.mobileledger.json.v1_23.TransactionListParser(input);
+ default:
+ throw new RuntimeException("Unsupported version " + apiVersion.toString());
+ }
+
+ }
+ abstract public LedgerTransaction nextTransaction() throws ParseException;
+}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.json.v1_14;
-import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
+import net.ktnx.mobileledger.json.API;
+
import java.io.IOException;
import java.io.InputStream;
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class AccountListParser {
-
- private final MappingIterator<ParsedLedgerAccount> iterator;
+public class AccountListParser extends net.ktnx.mobileledger.json.AccountListParser {
public AccountListParser(InputStream input) throws IOException {
ObjectMapper mapper = new ObjectMapper();
iterator = reader.readValues(input);
}
- public ParsedLedgerAccount nextAccount() {
- if (!iterator.hasNext()) return null;
-
- ParsedLedgerAccount next = iterator.next();
-
- if (next.getAname().equalsIgnoreCase("root")) return nextAccount();
-
- debug("accounts", String.format("Got account '%s'", next.getAname()));
- return next;
+ @Override
+ public API getApiVersion() {
+ return API.v1_14;
}
}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_14;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+public class Gateway extends net.ktnx.mobileledger.json.Gateway {
+ @Override
+ public String transactionSaveRequest(LedgerTransaction ledgerTransaction)
+ throws JsonProcessingException {
+ net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction jsonTransaction =
+ net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction.fromLedgerTransaction(
+ ledgerTransaction);
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectWriter writer =
+ mapper.writerFor(net.ktnx.mobileledger.json.v1_14.ParsedLedgerTransaction.class);
+ return writer.writeValueAsString(jsonTransaction);
+ }
+}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.json.v1_14;
-import androidx.annotation.NonNull;
-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedBalance {
- private ParsedQuantity aquantity;
- private String acommodity;
+public class ParsedBalance extends net.ktnx.mobileledger.json.ParsedBalance {
private ParsedStyle astyle;
public ParsedBalance() {
}
- public ParsedQuantity getAquantity() {
- return aquantity;
- }
- public void setAquantity(ParsedQuantity aquantity) {
- this.aquantity = aquantity;
- }
- @NonNull
- public String getAcommodity() {
- return (acommodity == null) ? "" : acommodity;
- }
- public void setAcommodity(String acommodity) {
- this.acommodity = acommodity;
- }
public ParsedStyle getAstyle() {
return astyle;
}
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.ArrayList;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedLedgerAccount {
+public class ParsedLedgerAccount extends net.ktnx.mobileledger.json.ParsedLedgerAccount {
private List<ParsedBalance> aebalance;
private List<ParsedBalance> aibalance;
- private String aname;
- private int anumpostings;
public ParsedLedgerAccount() {
}
- public int getAnumpostings() {
- return anumpostings;
- }
- public void setAnumpostings(int anumpostings) {
- this.anumpostings = anumpostings;
- }
public List<ParsedBalance> getAebalance() {
return aebalance;
}
public void setAibalance(List<ParsedBalance> aibalance) {
this.aibalance = aibalance;
}
- public String getAname() {
- return aname;
- }
- public void setAname(String aname) {
- this.aname = aname;
- }
+ @Override
+ public List<SimpleBalance> getSimpleBalance() {
+ List<SimpleBalance> result = new ArrayList<SimpleBalance>();
+ for (ParsedBalance b : getAibalance()) {
+ result.add(new SimpleBalance(b.getAcommodity(), b.getAquantity()
+ .asFloat()));
+ }
+ return result;
+ }
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
ParsedLedgerTransaction
result = new ParsedLedgerTransaction();
- result.setTcomment(tr.getComment());
+ result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
result.setTprecedingcomment("");
ArrayList<ParsedPosting> postings = new ArrayList<>();
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedStyle {
+public class ParsedStyle extends net.ktnx.mobileledger.json.ParsedStyle {
private int asprecision;
- private char asdecimalpoint;
- private char ascommodityside;
- private int digitgroups;
- private boolean ascommodityspaced;
public ParsedStyle() {
}
public int getAsprecision() {
public void setAsprecision(int asprecision) {
this.asprecision = asprecision;
}
- public char getAsdecimalpoint() {
- return asdecimalpoint;
- }
- public void setAsdecimalpoint(char asdecimalpoint) {
- this.asdecimalpoint = asdecimalpoint;
- }
- public char getAscommodityside() {
- return ascommodityside;
- }
- public void setAscommodityside(char ascommodityside) {
- this.ascommodityside = ascommodityside;
- }
- public int getDigitgroups() {
- return digitgroups;
- }
- public void setDigitgroups(int digitgroups) {
- this.digitgroups = digitgroups;
- }
- public boolean isAscommodityspaced() {
- return ascommodityspaced;
- }
- public void setAscommodityspaced(boolean ascommodityspaced) {
- this.ascommodityspaced = ascommodityspaced;
- }
}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
import java.io.IOException;
import java.io.InputStream;
+import java.text.ParseException;
-public class TransactionListParser {
+public class TransactionListParser extends net.ktnx.mobileledger.json.TransactionListParser {
private final MappingIterator<ParsedLedgerTransaction> iterator;
ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
iterator = reader.readValues(input);
}
- public ParsedLedgerTransaction nextTransaction() {
- return iterator.hasNext() ? iterator.next() : null;
+ public LedgerTransaction nextTransaction() throws ParseException {
+ return iterator.hasNext() ? iterator.next()
+ .asLedgerTransaction() : null;
}
}
package net.ktnx.mobileledger.json.v1_15;
-import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
+import net.ktnx.mobileledger.json.API;
+
import java.io.IOException;
import java.io.InputStream;
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class AccountListParser {
-
- private final MappingIterator<ParsedLedgerAccount> iterator;
+public class AccountListParser extends net.ktnx.mobileledger.json.AccountListParser {
public AccountListParser(InputStream input) throws IOException {
ObjectMapper mapper = new ObjectMapper();
iterator = reader.readValues(input);
}
- public ParsedLedgerAccount nextAccount() {
- if (!iterator.hasNext()) return null;
-
- ParsedLedgerAccount next = iterator.next();
-
- if (next.getAname().equalsIgnoreCase("root")) return nextAccount();
-
- debug("accounts", String.format("Got account '%s' [v1.15]", next.getAname()));
- return next;
+ @Override
+ public API getApiVersion() {
+ return API.v1_15;
}
}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_15;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+public class Gateway extends net.ktnx.mobileledger.json.Gateway {
+ @Override
+ public String transactionSaveRequest(LedgerTransaction ledgerTransaction)
+ throws JsonProcessingException {
+ net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction jsonTransaction =
+ net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.fromLedgerTransaction(
+ ledgerTransaction);
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectWriter writer =
+ mapper.writerFor(net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.class);
+ return writer.writeValueAsString(jsonTransaction);
+ }
+}
package net.ktnx.mobileledger.json.v1_15;
-import androidx.annotation.NonNull;
-
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedBalance {
- private ParsedQuantity aquantity;
- private String acommodity;
+public class ParsedBalance extends net.ktnx.mobileledger.json.ParsedBalance {
private ParsedStyle astyle;
public ParsedBalance() {
}
- public ParsedQuantity getAquantity() {
- return aquantity;
- }
- public void setAquantity(ParsedQuantity aquantity) {
- this.aquantity = aquantity;
- }
- @NonNull
- public String getAcommodity() {
- return (acommodity == null) ? "" : acommodity;
- }
- public void setAcommodity(String acommodity) {
- this.acommodity = acommodity;
- }
public ParsedStyle getAstyle() {
return astyle;
}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import java.util.List;
-
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedLedgerAccount {
- private List<ParsedBalance> aebalance;
- private List<ParsedBalance> aibalance;
- private String aname;
- private int anumpostings;
- public ParsedLedgerAccount() {
- }
- public int getAnumpostings() {
- return anumpostings;
- }
- public void setAnumpostings(int anumpostings) {
- this.anumpostings = anumpostings;
- }
- public List<ParsedBalance> getAebalance() {
- return aebalance;
- }
- public void setAebalance(List<ParsedBalance> aebalance) {
- this.aebalance = aebalance;
- }
- public List<ParsedBalance> getAibalance() {
- return aibalance;
- }
- public void setAibalance(List<ParsedBalance> aibalance) {
- this.aibalance = aibalance;
- }
- public String getAname() {
- return aname;
- }
- public void setAname(String aname) {
- this.aname = aname;
- }
-
-}
+public class ParsedLedgerAccount extends net.ktnx.mobileledger.json.v1_14.ParsedLedgerAccount {}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
}
public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
ParsedLedgerTransaction result = new ParsedLedgerTransaction();
- result.setTcomment(tr.getComment());
+ result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
result.setTprecedingcomment("");
ArrayList<ParsedPosting> postings = new ArrayList<>();
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedQuantity {
- private long decimalMantissa;
- private int decimalPlaces;
- public ParsedQuantity() {
- }
- public ParsedQuantity(String input) {
- parseString(input);
- }
- public long getDecimalMantissa() {
- return decimalMantissa;
- }
- public void setDecimalMantissa(long decimalMantissa) {
- this.decimalMantissa = decimalMantissa;
- }
- public int getDecimalPlaces() {
- return decimalPlaces;
- }
- public void setDecimalPlaces(int decimalPlaces) {
- this.decimalPlaces = decimalPlaces;
- }
- public float asFloat() {
- return (float) (decimalMantissa * Math.pow(10, -decimalPlaces));
- }
- public void parseString(String input) {
- int pointPos = input.indexOf('.');
- if (pointPos >= 0) {
- String integral = input.replace(".", "");
- decimalMantissa = Long.parseLong(integral);
- decimalPlaces = input.length() - pointPos - 1;
- }
- else {
- decimalMantissa = Long.parseLong(input);
- decimalPlaces = 0;
- }
- }
+public class ParsedQuantity extends net.ktnx.mobileledger.json.ParsedQuantity {
}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedStyle {
- private int asprecision;
- private char asdecimalpoint;
- private char ascommodityside;
- private int digitgroups;
- private boolean ascommodityspaced;
- public ParsedStyle() {
- }
- public int getAsprecision() {
- return asprecision;
- }
- public void setAsprecision(int asprecision) {
- this.asprecision = asprecision;
- }
- public char getAsdecimalpoint() {
- return asdecimalpoint;
- }
- public void setAsdecimalpoint(char asdecimalpoint) {
- this.asdecimalpoint = asdecimalpoint;
- }
- public char getAscommodityside() {
- return ascommodityside;
- }
- public void setAscommodityside(char ascommodityside) {
- this.ascommodityside = ascommodityside;
- }
- public int getDigitgroups() {
- return digitgroups;
- }
- public void setDigitgroups(int digitgroups) {
- this.digitgroups = digitgroups;
- }
- public boolean isAscommodityspaced() {
- return ascommodityspaced;
- }
- public void setAscommodityspaced(boolean ascommodityspaced) {
- this.ascommodityspaced = ascommodityspaced;
- }
-}
+public class ParsedStyle extends net.ktnx.mobileledger.json.v1_14.ParsedStyle {}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2020 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
import java.io.IOException;
import java.io.InputStream;
+import java.text.ParseException;
-public class TransactionListParser {
+public class TransactionListParser extends net.ktnx.mobileledger.json.TransactionListParser {
private final MappingIterator<ParsedLedgerTransaction> iterator;
ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
iterator = reader.readValues(input);
}
- public ParsedLedgerTransaction nextTransaction() {
- return iterator.hasNext() ? iterator.next() : null;
+ public LedgerTransaction nextTransaction() throws ParseException {
+ return iterator.hasNext() ? iterator.next()
+ .asLedgerTransaction() : null;
}
}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import net.ktnx.mobileledger.json.API;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountListParser extends net.ktnx.mobileledger.json.AccountListParser {
+
+ public AccountListParser(InputStream input) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectReader reader = mapper.readerFor(ParsedLedgerAccount.class);
+
+ iterator = reader.readValues(input);
+ }
+ @Override
+ public API getApiVersion() {
+ return API.v1_19_1;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+public class Gateway extends net.ktnx.mobileledger.json.Gateway {
+ @Override
+ public String transactionSaveRequest(LedgerTransaction ledgerTransaction)
+ throws JsonProcessingException {
+ net.ktnx.mobileledger.json.v1_19_1.ParsedLedgerTransaction jsonTransaction =
+ net.ktnx.mobileledger.json.v1_19_1.ParsedLedgerTransaction.fromLedgerTransaction(
+ ledgerTransaction);
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectWriter writer =
+ mapper.writerFor(net.ktnx.mobileledger.json.v1_19_1.ParsedLedgerTransaction.class);
+ return writer.writeValueAsString(jsonTransaction);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedAmount {
+ private String acommodity;
+ private ParsedQuantity aquantity;
+ private boolean aismultiplier;
+ private ParsedStyle astyle;
+ private ParsedPrice aprice;
+ public ParsedAmount() {
+ }
+ public ParsedPrice getAprice() {
+ return aprice;
+ }
+ public void setAprice(ParsedPrice aprice) {
+ this.aprice = aprice;
+ }
+ public String getAcommodity() {
+ return acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+ public ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ public boolean isAismultiplier() {
+ return aismultiplier;
+ }
+ public void setAismultiplier(boolean aismultiplier) {
+ this.aismultiplier = aismultiplier;
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedBalance extends net.ktnx.mobileledger.json.ParsedBalance {
+ private ParsedStyle astyle;
+ public ParsedBalance() {
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedLedgerAccount extends net.ktnx.mobileledger.json.ParsedLedgerAccount {
+ private List<ParsedBalance> aebalance;
+ private List<ParsedBalance> aibalance;
+ public ParsedLedgerAccount() {
+ }
+ public List<ParsedBalance> getAibalance() {
+ return aibalance;
+ }
+ public void setAibalance(List<ParsedBalance> aibalance) {
+ this.aibalance = aibalance;
+ }
+ public List<ParsedBalance> getAebalance() {
+ return aebalance;
+ }
+ public void setAebalance(List<ParsedBalance> aebalance) {
+ this.aebalance = aebalance;
+ }
+ @Override
+ public List<SimpleBalance> getSimpleBalance() {
+ List<SimpleBalance> result = new ArrayList<SimpleBalance>();
+ for (ParsedBalance b : getAibalance()) {
+ result.add(new SimpleBalance(b.getAcommodity(), b.getAquantity()
+ .asFloat()));
+ }
+
+ return result;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.ParsedLedgerTransaction {
+ private String tdate;
+ private String tdate2 = null;
+ private String tdescription;
+ private String tcomment;
+ private String tcode = "";
+ private String tstatus = "Unmarked";
+ private String tprecedingcomment = "";
+ private int tindex;
+ private List<ParsedPosting> tpostings;
+ private List<List<String>> ttags = new ArrayList<>();
+ private ParsedSourcePos tsourcepos = new ParsedSourcePos();
+ public ParsedLedgerTransaction() {
+ }
+ public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
+ ParsedLedgerTransaction result = new ParsedLedgerTransaction();
+ result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
+ result.setTprecedingcomment("");
+
+ ArrayList<ParsedPosting> postings = new ArrayList<>();
+ for (LedgerTransactionAccount acc : tr.getAccounts()) {
+ if (!acc.getAccountName()
+ .isEmpty())
+ postings.add(ParsedPosting.fromLedgerAccount(acc));
+ }
+
+ result.setTpostings(postings);
+ SimpleDate transactionDate = tr.getDateIfAny();
+ if (transactionDate == null) {
+ transactionDate = SimpleDate.today();
+ }
+ result.setTdate(Globals.formatIsoDate(transactionDate));
+ result.setTdate2(null);
+ result.setTindex(1);
+ result.setTdescription(tr.getDescription());
+ return result;
+ }
+ public String getTcode() {
+ return tcode;
+ }
+ public void setTcode(String tcode) {
+ this.tcode = tcode;
+ }
+ public String getTstatus() {
+ return tstatus;
+ }
+ public void setTstatus(String tstatus) {
+ this.tstatus = tstatus;
+ }
+ public List<List<String>> getTtags() {
+ return ttags;
+ }
+ public void setTtags(List<List<String>> ttags) {
+ this.ttags = ttags;
+ }
+ public ParsedSourcePos getTsourcepos() {
+ return tsourcepos;
+ }
+ public void setTsourcepos(ParsedSourcePos tsourcepos) {
+ this.tsourcepos = tsourcepos;
+ }
+ public String getTprecedingcomment() {
+ return tprecedingcomment;
+ }
+ public void setTprecedingcomment(String tprecedingcomment) {
+ this.tprecedingcomment = tprecedingcomment;
+ }
+ public String getTdate() {
+ return tdate;
+ }
+ public void setTdate(String tdate) {
+ this.tdate = tdate;
+ }
+ public String getTdate2() {
+ return tdate2;
+ }
+ public void setTdate2(String tdate2) {
+ this.tdate2 = tdate2;
+ }
+ public String getTdescription() {
+ return tdescription;
+ }
+ public void setTdescription(String tdescription) {
+ this.tdescription = tdescription;
+ }
+ public String getTcomment() {
+ return tcomment;
+ }
+ public void setTcomment(String tcomment) {
+ this.tcomment = tcomment;
+ }
+ public int getTindex() {
+ return tindex;
+ }
+ public void setTindex(int tindex) {
+ this.tindex = tindex;
+ if (tpostings != null)
+ for (ParsedPosting p : tpostings) {
+ p.setPtransaction_(tindex);
+ }
+ }
+ public List<ParsedPosting> getTpostings() {
+ return tpostings;
+ }
+ public void setTpostings(List<ParsedPosting> tpostings) {
+ this.tpostings = tpostings;
+ }
+ public void addPosting(ParsedPosting posting) {
+ posting.setPtransaction_(tindex);
+ tpostings.add(posting);
+ }
+ public LedgerTransaction asLedgerTransaction() throws ParseException {
+ SimpleDate date = Globals.parseIsoDate(tdate);
+ LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
+ tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
+
+ List<ParsedPosting> postings = tpostings;
+
+ if (postings != null) {
+ for (ParsedPosting p : postings) {
+ tr.addAccount(p.asLedgerAccount());
+ }
+ }
+
+ tr.markDataAsLoaded();
+ return tr;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedPosting extends net.ktnx.mobileledger.json.ParsedPosting {
+ private Void pbalanceassertion;
+ private String pstatus = "Unmarked";
+ private String paccount;
+ private List<ParsedAmount> pamount;
+ private String pdate = null;
+ private String pdate2 = null;
+ private String ptype = "RegularPosting";
+ private String pcomment = "";
+ private List<List<String>> ptags = new ArrayList<>();
+ private String poriginal = null;
+ private int ptransaction_;
+ public ParsedPosting() {
+ }
+ public static ParsedPosting fromLedgerAccount(LedgerTransactionAccount acc) {
+ ParsedPosting result = new ParsedPosting();
+ result.setPaccount(acc.getAccountName());
+
+ String comment = acc.getComment();
+ if (comment == null)
+ comment = "";
+ result.setPcomment(comment);
+
+ ArrayList<ParsedAmount> amounts = new ArrayList<>();
+ ParsedAmount amt = new ParsedAmount();
+ amt.setAcommodity((acc.getCurrency() == null) ? "" : acc.getCurrency());
+ amt.setAismultiplier(false);
+ ParsedQuantity qty = new ParsedQuantity();
+ qty.setDecimalPlaces(2);
+ qty.setDecimalMantissa(Math.round(acc.getAmount() * 100));
+ amt.setAquantity(qty);
+ ParsedStyle style = new ParsedStyle();
+ style.setAscommodityside(getCommoditySide());
+ style.setAscommodityspaced(getCommoditySpaced());
+ style.setAsprecision(new ParsedPrecision(2));
+ style.setAsdecimalpoint('.');
+ amt.setAstyle(style);
+ if (acc.getCurrency() != null)
+ amt.setAcommodity(acc.getCurrency());
+ amounts.add(amt);
+ result.setPamount(amounts);
+ return result;
+ }
+ public String getPdate2() {
+ return pdate2;
+ }
+ public void setPdate2(String pdate2) {
+ this.pdate2 = pdate2;
+ }
+ public int getPtransaction_() {
+ return ptransaction_;
+ }
+ public void setPtransaction_(int ptransaction_) {
+ this.ptransaction_ = ptransaction_;
+ }
+ public String getPdate() {
+ return pdate;
+ }
+ public void setPdate(String pdate) {
+ this.pdate = pdate;
+ }
+ public String getPtype() {
+ return ptype;
+ }
+ public void setPtype(String ptype) {
+ this.ptype = ptype;
+ }
+ public String getPcomment() {
+ return pcomment;
+ }
+ public void setPcomment(String pcomment) {
+ this.pcomment = (pcomment == null) ? null : pcomment.trim();
+ }
+ public List<List<String>> getPtags() {
+ return ptags;
+ }
+ public void setPtags(List<List<String>> ptags) {
+ this.ptags = ptags;
+ }
+ public String getPoriginal() {
+ return poriginal;
+ }
+ public void setPoriginal(String poriginal) {
+ this.poriginal = poriginal;
+ }
+ public String getPstatus() {
+ return pstatus;
+ }
+ public void setPstatus(String pstatus) {
+ this.pstatus = pstatus;
+ }
+ public Void getPbalanceassertion() {
+ return pbalanceassertion;
+ }
+ public void setPbalanceassertion(Void pbalanceassertion) {
+ this.pbalanceassertion = pbalanceassertion;
+ }
+ public String getPaccount() {
+ return paccount;
+ }
+ public void setPaccount(String paccount) {
+ this.paccount = paccount;
+ }
+ public List<ParsedAmount> getPamount() {
+ return pamount;
+ }
+ public void setPamount(List<ParsedAmount> pamount) {
+ this.pamount = pamount;
+ }
+ public LedgerTransactionAccount asLedgerAccount() {
+ ParsedAmount amt = pamount.get(0);
+ return new LedgerTransactionAccount(paccount, amt.getAquantity()
+ .asFloat(), amt.getAcommodity(),
+ getPcomment());
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+class ParsedPrecision {
+ private int contents;
+ private String tag;
+ ParsedPrecision() {
+ tag = "NaturalPrecision";
+ }
+ ParsedPrecision(int contents) {
+ this.contents = contents;
+ tag = "Precision";
+ }
+ public int getContents() {
+ return contents;
+ }
+ public void setContents(int contents) {
+ this.contents = contents;
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+class ParsedPrice {
+ private String tag;
+ private Contents contents;
+ public ParsedPrice() {
+ tag = "NoPrice";
+ }
+ public Contents getContents() {
+ return contents;
+ }
+ public void setContents(Contents contents) {
+ this.contents = contents;
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+ private static class Contents {
+ private ParsedPrice aprice;
+ private ParsedQuantity aquantity;
+ private String acommodity;
+ private boolean aismultiplier;
+ private ParsedStyle astyle;
+ public Contents() {
+ acommodity = "";
+ }
+ public ParsedPrice getAprice() {
+ return aprice;
+ }
+ public void setAprice(ParsedPrice aprice) {
+ this.aprice = aprice;
+ }
+ public ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ public String getAcommodity() {
+ return acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+ public boolean isAismultiplier() {
+ return aismultiplier;
+ }
+ public void setAismultiplier(boolean aismultiplier) {
+ this.aismultiplier = aismultiplier;
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedQuantity extends net.ktnx.mobileledger.json.ParsedQuantity {}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class ParsedSourcePos {
+ private String tag = "JournalSourcePos";
+ private List<Object> contents;
+ public ParsedSourcePos() {
+ contents = new ArrayList<>();
+ contents.add("");
+ contents.add(new Integer[]{1, 1});
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+ public List<Object> getContents() {
+ return contents;
+ }
+ public void setContents(List<Object> contents) {
+ this.contents = contents;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedStyle extends net.ktnx.mobileledger.json.ParsedStyle {
+ private ParsedPrecision asprecision;
+ public ParsedStyle() {
+ }
+ public ParsedPrecision getAsprecision() {
+ return asprecision;
+ }
+ public void setAsprecision(ParsedPrecision asprecision) {
+ this.asprecision = asprecision;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_19_1;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+
+public class TransactionListParser extends net.ktnx.mobileledger.json.TransactionListParser {
+
+ private final MappingIterator<ParsedLedgerTransaction> iterator;
+
+ public TransactionListParser(InputStream input) throws IOException {
+
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
+ iterator = reader.readValues(input);
+ }
+ public LedgerTransaction nextTransaction() throws ParseException {
+ return iterator.hasNext() ? iterator.next()
+ .asLedgerTransaction() : null;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import net.ktnx.mobileledger.json.API;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountListParser extends net.ktnx.mobileledger.json.AccountListParser {
+
+ public AccountListParser(InputStream input) throws IOException {
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectReader reader = mapper.readerFor(ParsedLedgerAccount.class);
+
+ iterator = reader.readValues(input);
+ }
+ @Override
+ public API getApiVersion() {
+ return API.v1_19_1;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+public class Gateway extends net.ktnx.mobileledger.json.Gateway {
+ @Override
+ public String transactionSaveRequest(LedgerTransaction ledgerTransaction)
+ throws JsonProcessingException {
+ ParsedLedgerTransaction jsonTransaction =
+ ParsedLedgerTransaction.fromLedgerTransaction(ledgerTransaction);
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectWriter writer = mapper.writerFor(ParsedLedgerTransaction.class);
+ return writer.writeValueAsString(jsonTransaction);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedAmount {
+ private String acommodity;
+ private ParsedQuantity aquantity;
+ private boolean aismultiplier;
+ private ParsedStyle astyle;
+ private ParsedPrice aprice;
+ public ParsedAmount() {
+ }
+ public ParsedPrice getAprice() {
+ return aprice;
+ }
+ public void setAprice(ParsedPrice aprice) {
+ this.aprice = aprice;
+ }
+ public String getAcommodity() {
+ return acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+ public ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ public boolean isAismultiplier() {
+ return aismultiplier;
+ }
+ public void setAismultiplier(boolean aismultiplier) {
+ this.aismultiplier = aismultiplier;
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedBalance extends net.ktnx.mobileledger.json.ParsedBalance {
+ private ParsedStyle astyle;
+ public ParsedBalance() {
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedLedgerAccount extends net.ktnx.mobileledger.json.ParsedLedgerAccount {
+ private List<ParsedBalance> aebalance;
+ private List<ParsedBalance> aibalance;
+ public ParsedLedgerAccount() {
+ }
+ public List<ParsedBalance> getAibalance() {
+ return aibalance;
+ }
+ public void setAibalance(List<ParsedBalance> aibalance) {
+ this.aibalance = aibalance;
+ }
+ public List<ParsedBalance> getAebalance() {
+ return aebalance;
+ }
+ public void setAebalance(List<ParsedBalance> aebalance) {
+ this.aebalance = aebalance;
+ }
+ @Override
+ public List<SimpleBalance> getSimpleBalance() {
+ List<SimpleBalance> result = new ArrayList<SimpleBalance>();
+ for (ParsedBalance b : getAibalance()) {
+ result.add(new SimpleBalance(b.getAcommodity(), b.getAquantity()
+ .asFloat()));
+ }
+
+ return result;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.ParsedLedgerTransaction {
+ private String tdate;
+ private String tdate2 = null;
+ private String tdescription;
+ private String tcomment;
+ private String tcode = "";
+ private String tstatus = "Unmarked";
+ private String tprecedingcomment = "";
+ private int tindex;
+ private List<ParsedPosting> tpostings;
+ private List<List<String>> ttags = new ArrayList<>();
+ private List<ParsedSourcePos> tsourcepos = new ArrayList<>();
+ public ParsedLedgerTransaction() {
+ ParsedSourcePos startPos = new ParsedSourcePos();
+ ParsedSourcePos endPos = new ParsedSourcePos();
+ endPos.setSourceLine(2);
+
+ tsourcepos.add(startPos);
+ tsourcepos.add(endPos);
+ }
+ public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
+ ParsedLedgerTransaction result = new ParsedLedgerTransaction();
+ result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
+ result.setTprecedingcomment("");
+
+ ArrayList<ParsedPosting> postings = new ArrayList<>();
+ for (LedgerTransactionAccount acc : tr.getAccounts()) {
+ if (!acc.getAccountName()
+ .isEmpty())
+ postings.add(ParsedPosting.fromLedgerAccount(acc));
+ }
+
+ result.setTpostings(postings);
+ SimpleDate transactionDate = tr.getDateIfAny();
+ if (transactionDate == null) {
+ transactionDate = SimpleDate.today();
+ }
+ result.setTdate(Globals.formatIsoDate(transactionDate));
+ result.setTdate2(null);
+ result.setTindex(1);
+ result.setTdescription(tr.getDescription());
+ return result;
+ }
+ public String getTcode() {
+ return tcode;
+ }
+ public void setTcode(String tcode) {
+ this.tcode = tcode;
+ }
+ public String getTstatus() {
+ return tstatus;
+ }
+ public void setTstatus(String tstatus) {
+ this.tstatus = tstatus;
+ }
+ public List<List<String>> getTtags() {
+ return ttags;
+ }
+ public void setTtags(List<List<String>> ttags) {
+ this.ttags = ttags;
+ }
+ public List<ParsedSourcePos> getTsourcepos() {
+ return tsourcepos;
+ }
+ public void setTsourcepos(List<ParsedSourcePos> tsourcepos) {
+ this.tsourcepos = tsourcepos;
+ }
+ public String getTprecedingcomment() {
+ return tprecedingcomment;
+ }
+ public void setTprecedingcomment(String tprecedingcomment) {
+ this.tprecedingcomment = tprecedingcomment;
+ }
+ public String getTdate() {
+ return tdate;
+ }
+ public void setTdate(String tdate) {
+ this.tdate = tdate;
+ }
+ public String getTdate2() {
+ return tdate2;
+ }
+ public void setTdate2(String tdate2) {
+ this.tdate2 = tdate2;
+ }
+ public String getTdescription() {
+ return tdescription;
+ }
+ public void setTdescription(String tdescription) {
+ this.tdescription = tdescription;
+ }
+ public String getTcomment() {
+ return tcomment;
+ }
+ public void setTcomment(String tcomment) {
+ this.tcomment = tcomment;
+ }
+ public int getTindex() {
+ return tindex;
+ }
+ public void setTindex(int tindex) {
+ this.tindex = tindex;
+ if (tpostings != null)
+ for (ParsedPosting p : tpostings) {
+ p.setPtransaction_(tindex);
+ }
+ }
+ public List<ParsedPosting> getTpostings() {
+ return tpostings;
+ }
+ public void setTpostings(List<ParsedPosting> tpostings) {
+ this.tpostings = tpostings;
+ }
+ public void addPosting(ParsedPosting posting) {
+ posting.setPtransaction_(tindex);
+ tpostings.add(posting);
+ }
+ public LedgerTransaction asLedgerTransaction() throws ParseException {
+ SimpleDate date = Globals.parseIsoDate(tdate);
+ LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
+ tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
+
+ List<ParsedPosting> postings = tpostings;
+
+ if (postings != null) {
+ for (ParsedPosting p : postings) {
+ tr.addAccount(p.asLedgerAccount());
+ }
+ }
+
+ tr.markDataAsLoaded();
+ return tr;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedPosting extends net.ktnx.mobileledger.json.ParsedPosting {
+ private Void pbalanceassertion;
+ private String pstatus = "Unmarked";
+ private String paccount;
+ private List<ParsedAmount> pamount;
+ private String pdate = null;
+ private String pdate2 = null;
+ private String ptype = "RegularPosting";
+ private String pcomment = "";
+ private List<List<String>> ptags = new ArrayList<>();
+ private String poriginal = null;
+ private int ptransaction_;
+ public ParsedPosting() {
+ }
+ public static ParsedPosting fromLedgerAccount(LedgerTransactionAccount acc) {
+ ParsedPosting result = new ParsedPosting();
+ result.setPaccount(acc.getAccountName());
+
+ String comment = acc.getComment();
+ if (comment == null)
+ comment = "";
+ result.setPcomment(comment);
+
+ ArrayList<ParsedAmount> amounts = new ArrayList<>();
+ ParsedAmount amt = new ParsedAmount();
+ amt.setAcommodity((acc.getCurrency() == null) ? "" : acc.getCurrency());
+ amt.setAismultiplier(false);
+ ParsedQuantity qty = new ParsedQuantity();
+ qty.setDecimalPlaces(2);
+ qty.setDecimalMantissa(Math.round(acc.getAmount() * 100));
+ amt.setAquantity(qty);
+ ParsedStyle style = new ParsedStyle();
+ style.setAscommodityside(getCommoditySide());
+ style.setAscommodityspaced(getCommoditySpaced());
+ style.setAsprecision(2);
+ style.setAsdecimalpoint('.');
+ amt.setAstyle(style);
+ if (acc.getCurrency() != null)
+ amt.setAcommodity(acc.getCurrency());
+ amounts.add(amt);
+ result.setPamount(amounts);
+ return result;
+ }
+ public String getPdate2() {
+ return pdate2;
+ }
+ public void setPdate2(String pdate2) {
+ this.pdate2 = pdate2;
+ }
+ public int getPtransaction_() {
+ return ptransaction_;
+ }
+ public void setPtransaction_(int ptransaction_) {
+ this.ptransaction_ = ptransaction_;
+ }
+ public String getPdate() {
+ return pdate;
+ }
+ public void setPdate(String pdate) {
+ this.pdate = pdate;
+ }
+ public String getPtype() {
+ return ptype;
+ }
+ public void setPtype(String ptype) {
+ this.ptype = ptype;
+ }
+ public String getPcomment() {
+ return pcomment;
+ }
+ public void setPcomment(String pcomment) {
+ this.pcomment = (pcomment == null) ? null : pcomment.trim();
+ }
+ public List<List<String>> getPtags() {
+ return ptags;
+ }
+ public void setPtags(List<List<String>> ptags) {
+ this.ptags = ptags;
+ }
+ public String getPoriginal() {
+ return poriginal;
+ }
+ public void setPoriginal(String poriginal) {
+ this.poriginal = poriginal;
+ }
+ public String getPstatus() {
+ return pstatus;
+ }
+ public void setPstatus(String pstatus) {
+ this.pstatus = pstatus;
+ }
+ public Void getPbalanceassertion() {
+ return pbalanceassertion;
+ }
+ public void setPbalanceassertion(Void pbalanceassertion) {
+ this.pbalanceassertion = pbalanceassertion;
+ }
+ public String getPaccount() {
+ return paccount;
+ }
+ public void setPaccount(String paccount) {
+ this.paccount = paccount;
+ }
+ public List<ParsedAmount> getPamount() {
+ return pamount;
+ }
+ public void setPamount(List<ParsedAmount> pamount) {
+ this.pamount = pamount;
+ }
+ public LedgerTransactionAccount asLedgerAccount() {
+ ParsedAmount amt = pamount.get(0);
+ return new LedgerTransactionAccount(paccount, amt.getAquantity()
+ .asFloat(), amt.getAcommodity(),
+ getPcomment());
+ }
+
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+class ParsedPrice {
+ private String tag;
+ private Contents contents;
+ public ParsedPrice() {
+ tag = "NoPrice";
+ }
+ public Contents getContents() {
+ return contents;
+ }
+ public void setContents(Contents contents) {
+ this.contents = contents;
+ }
+ public String getTag() {
+ return tag;
+ }
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+ private static class Contents {
+ private ParsedPrice aprice;
+ private ParsedQuantity aquantity;
+ private String acommodity;
+ private boolean aismultiplier;
+ private ParsedStyle astyle;
+ public Contents() {
+ acommodity = "";
+ }
+ public ParsedPrice getAprice() {
+ return aprice;
+ }
+ public void setAprice(ParsedPrice aprice) {
+ this.aprice = aprice;
+ }
+ public ParsedQuantity getAquantity() {
+ return aquantity;
+ }
+ public void setAquantity(ParsedQuantity aquantity) {
+ this.aquantity = aquantity;
+ }
+ public String getAcommodity() {
+ return acommodity;
+ }
+ public void setAcommodity(String acommodity) {
+ this.acommodity = acommodity;
+ }
+ public boolean isAismultiplier() {
+ return aismultiplier;
+ }
+ public void setAismultiplier(boolean aismultiplier) {
+ this.aismultiplier = aismultiplier;
+ }
+ public ParsedStyle getAstyle() {
+ return astyle;
+ }
+ public void setAstyle(ParsedStyle astyle) {
+ this.astyle = astyle;
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedQuantity extends net.ktnx.mobileledger.json.ParsedQuantity {}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+class ParsedSourcePos {
+ private String sourceName = "";
+ private int sourceLine = 1;
+ private int sourceColumn = 1;
+ public ParsedSourcePos() {
+ }
+ public String getSourceName() {
+ return sourceName;
+ }
+ public void setSourceName(String sourceName) {
+ this.sourceName = sourceName;
+ }
+ public int getSourceLine() {
+ return sourceLine;
+ }
+ public void setSourceLine(int sourceLine) {
+ this.sourceLine = sourceLine;
+ }
+ public int getSourceColumn() {
+ return sourceColumn;
+ }
+ public void setSourceColumn(int sourceColumn) {
+ this.sourceColumn = sourceColumn;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ParsedStyle extends net.ktnx.mobileledger.json.ParsedStyle {
+ private int asprecision;
+ public ParsedStyle() {
+ }
+ public int getAsprecision() {
+ return asprecision;
+ }
+ public void setAsprecision(int asprecision) {
+ this.asprecision = asprecision;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2020 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.json.v1_23;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+import net.ktnx.mobileledger.model.LedgerTransaction;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+
+public class TransactionListParser extends net.ktnx.mobileledger.json.TransactionListParser {
+
+ private final MappingIterator<ParsedLedgerTransaction> iterator;
+
+ public TransactionListParser(InputStream input) throws IOException {
+
+ ObjectMapper mapper = new ObjectMapper();
+ ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
+ iterator = reader.readValues(input);
+ }
+ public LedgerTransaction nextTransaction() throws ParseException {
+ return iterator.hasNext() ? iterator.next()
+ .asLedgerTransaction() : null;
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.model;
import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
import org.jetbrains.annotations.NotNull;
-public class AccountListItem {
- private final Type type;
- private LedgerAccount account;
- public AccountListItem(@NotNull LedgerAccount account) {
- this.type = Type.ACCOUNT;
- this.account = account;
- }
- public AccountListItem() {
- this.type = Type.HEADER;
- }
+public abstract class AccountListItem {
+ private AccountListItem() {}
+ public abstract boolean sameContent(AccountListItem other);
@NonNull
public Type getType() {
- return type;
+ if (this instanceof Account)
+ return Type.ACCOUNT;
+ else if (this instanceof Header)
+ return Type.HEADER;
+ else
+ throw new RuntimeException("Unsupported sub-class " + this);
+ }
+ public boolean isAccount() {
+ return this instanceof Account;
+ }
+ public Account toAccount() {
+ assert isAccount();
+ return ((Account) this);
+ }
+ public boolean isHeader() {
+ return this instanceof Header;
}
- @NotNull
- public LedgerAccount getAccount() {
- if (type != Type.ACCOUNT)
- throw new IllegalStateException(
- String.format("Item type is not %s, but %s", Type.ACCOUNT, type));
- return account;
+ public Header toHeader() {
+ assert isHeader();
+ return ((Header) this);
}
public enum Type {ACCOUNT, HEADER}
+
+ public static class Account extends AccountListItem {
+ private final LedgerAccount account;
+ public Account(@NotNull LedgerAccount account) {
+ this.account = account;
+ }
+ @Override
+ public boolean sameContent(AccountListItem other) {
+ if (!(other instanceof Account))
+ return false;
+ return ((Account) other).account.hasSubAccounts() == account.hasSubAccounts() &&
+ ((Account) other).account.amountsExpanded() == account.amountsExpanded() &&
+ ((Account) other).account.isExpanded() == account.isExpanded() &&
+ ((Account) other).account.getLevel() == account.getLevel() &&
+ ((Account) other).account.getAmountsString()
+ .equals(account.getAmountsString());
+ }
+ @NotNull
+ public LedgerAccount getAccount() {
+ return account;
+ }
+ public boolean allAmountsAreZero() {
+ return account.allAmountsAreZero();
+ }
+ }
+
+ public static class Header extends AccountListItem {
+ private final LiveData<String> text;
+ public Header(@NonNull LiveData<String> text) {
+ this.text = text;
+ }
+ public LiveData<String> getText() {
+ return text;
+ }
+ @Override
+ public boolean sameContent(AccountListItem other) {
+ return true;
+ }
+ }
}
package net.ktnx.mobileledger.model;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.DiffUtil;
-
-import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.utils.Misc;
public class Currency {
- public static final DiffUtil.ItemCallback<Currency> DIFF_CALLBACK =
- new DiffUtil.ItemCallback<Currency>() {
- @Override
- public boolean areItemsTheSame(@NonNull Currency oldItem,
- @NonNull Currency newItem) {
- return oldItem.id == newItem.id;
- }
- @Override
- public boolean areContentsTheSame(@NonNull Currency oldItem,
- @NonNull Currency newItem) {
- return oldItem.name.equals(newItem.name) &&
- oldItem.position.equals(newItem.position) &&
- (oldItem.hasGap == newItem.hasGap);
- }
- };
private final int id;
private String name;
private Position position;
this.position = position;
this.hasGap = hasGap;
}
- public Currency(MobileLedgerProfile profile, String name, Position position, boolean hasGap) {
- SQLiteDatabase db = App.getDatabase();
-
- try (Cursor c = db.rawQuery("select max(rowid) from currencies", null)) {
- c.moveToNext();
- this.id = c.getInt(0) + 1;
- }
- db.execSQL("insert into currencies(id, name, position, has_gap) values(?, ?, ?, ?)",
- new Object[]{this.id, name, position.toString(), hasGap});
-
- this.name = name;
- this.position = position;
- this.hasGap = hasGap;
- }
- public static Currency loadByName(String name) {
- MobileLedgerProfile profile = Data.getProfile();
- return profile.loadCurrencyByName(name);
- }
static public boolean equal(Currency left, Currency right) {
if (left == null) {
return right == null;
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.model;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
-import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
-import net.ktnx.mobileledger.utils.LockHolder;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.utils.Locker;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.ObservableValue;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
-import java.util.ArrayList;
+import java.text.ParseException;
+import java.text.ParsePosition;
import java.util.Date;
import java.util.List;
import java.util.Locale;
-import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import static net.ktnx.mobileledger.utils.Logger.debug;
new MutableLiveData<>(false);
public static final MutableLiveData<RetrieveTransactionsTask.Progress> backgroundTaskProgress =
new MutableLiveData<>();
- public static final MutableLiveData<ArrayList<MobileLedgerProfile>> profiles =
- new MutableLiveData<>(null);
+ public static final LiveData<List<Profile>> profiles = DB.get()
+ .getProfileDAO()
+ .getAllOrdered();
public static final MutableLiveData<Currency.Position> currencySymbolPosition =
new MutableLiveData<>();
public static final MutableLiveData<Boolean> currencyGap = new MutableLiveData<>(true);
public static final MutableLiveData<Integer> lastUpdateTransactionCount =
new MutableLiveData<>(0);
public static final MutableLiveData<Integer> lastUpdateAccountCount = new MutableLiveData<>(0);
- public static final ObservableValue<String> lastTransactionsUpdateText =
- new ObservableValue<>();
- public static final ObservableValue<String> lastAccountsUpdateText = new ObservableValue<>();
- private static final MutableLiveData<MobileLedgerProfile> profile =
- new InertMutableLiveData<>();
+ public static final MutableLiveData<String> lastTransactionsUpdateText =
+ new MutableLiveData<>();
+ public static final MutableLiveData<String> lastAccountsUpdateText = new MutableLiveData<>();
+ public static final String decimalDot = ".";
+
+ private static final MutableLiveData<Profile> profile = new MutableLiveData<>();
private static final AtomicInteger backgroundTaskCount = new AtomicInteger(0);
private static final Locker profilesLocker = new Locker();
+ private static NumberFormat numberFormatter;
+ private static String decimalSeparator = "";
static {
locale.setValue(Locale.getDefault());
}
- @NonNull
- public static MobileLedgerProfile getProfile() {
- return Objects.requireNonNull(profile.getValue());
+ public static String getDecimalSeparator() {
+ return decimalSeparator;
+ }
+ @Nullable
+ public static Profile getProfile() {
+ return profile.getValue();
}
public static void backgroundTaskStarted() {
int cnt = backgroundTaskCount.incrementAndGet();
cnt));
backgroundTasksRunning.postValue(cnt > 0);
}
- public static void setCurrentProfile(@NonNull MobileLedgerProfile newProfile) {
- MLDB.setOption(MLDB.OPT_PROFILE_UUID, newProfile.getUuid());
- profile.postValue(newProfile);
- }
- public static int getProfileIndex(MobileLedgerProfile profile) {
- try (LockHolder ignored = profilesLocker.lockForReading()) {
- List<MobileLedgerProfile> prList = profiles.getValue();
- if (prList == null)
- throw new AssertionError();
- for (int i = 0; i < prList.size(); i++) {
- MobileLedgerProfile p = prList.get(i);
- if (p.equals(profile))
- return i;
- }
-
- return -1;
- }
- }
- @SuppressWarnings("WeakerAccess")
- public static int getProfileIndex(String profileUUID) {
- try (LockHolder ignored = profilesLocker.lockForReading()) {
- List<MobileLedgerProfile> prList = profiles.getValue();
- if (prList == null)
- throw new AssertionError();
- for (int i = 0; i < prList.size(); i++) {
- MobileLedgerProfile p = prList.get(i);
- if (p.getUuid()
- .equals(profileUUID))
- return i;
- }
-
- return -1;
- }
- }
- public static int retrieveCurrentThemeIdFromDb() {
- String profileUUID = MLDB.getOption(MLDB.OPT_PROFILE_UUID, null);
- if (profileUUID == null)
- return -1;
-
- SQLiteDatabase db = App.getDatabase();
- try (Cursor c = db.rawQuery("SELECT theme from profiles where uuid=?",
- new String[]{profileUUID}))
- {
- if (c.moveToNext())
- return c.getInt(0);
- }
-
- return -1;
+ public static void setCurrentProfile(Profile newProfile) {
+ profile.setValue(newProfile);
}
- @Nullable
- public static MobileLedgerProfile getProfile(String profileUUID) {
- MobileLedgerProfile profile;
- try (LockHolder readLock = profilesLocker.lockForReading()) {
- List<MobileLedgerProfile> prList = profiles.getValue();
- if ((prList == null) || prList.isEmpty()) {
- readLock.close();
- try (LockHolder ignored = profilesLocker.lockForWriting()) {
- profile = MobileLedgerProfile.loadAllFromDB(profileUUID);
- }
- }
- else {
- int i = getProfileIndex(profileUUID);
- if (i == -1)
- i = 0;
- profile = prList.get(i);
- }
- }
- return profile;
+ public static void postCurrentProfile(Profile newProfile) {
+ profile.postValue(newProfile);
}
public static void refreshCurrencyData(Locale locale) {
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
}
else
currencySymbolPosition.setValue(Currency.Position.none);
- }
- public static void observeProfile(LifecycleOwner lifecycleOwner,
- Observer<MobileLedgerProfile> observer) {
- profile.observe(lifecycleOwner, observer);
+ NumberFormat newNumberFormatter = NumberFormat.getNumberInstance();
+ newNumberFormatter.setParseIntegerOnly(false);
+ newNumberFormatter.setGroupingUsed(true);
+ newNumberFormatter.setMinimumIntegerDigits(1);
+ newNumberFormatter.setMinimumFractionDigits(2);
+
+ numberFormatter = newNumberFormatter;
+
+ decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance(locale)
+ .getMonetaryDecimalSeparator());
}
- public synchronized static MobileLedgerProfile initProfile() {
- MobileLedgerProfile currentProfile = profile.getValue();
- if (currentProfile != null)
- return currentProfile;
-
- String profileUUID = MLDB.getOption(MLDB.OPT_PROFILE_UUID, null);
- MobileLedgerProfile startupProfile = getProfile(profileUUID);
- if (startupProfile != null)
- setCurrentProfile(startupProfile);
- return startupProfile;
+ @NotNull
+ public static String formatCurrency(float number) {
+ NumberFormat formatter = NumberFormat.getCurrencyInstance(locale.getValue());
+ return formatter.format(number);
+ }
+ @NotNull
+ public static String formatNumber(float number) {
+ return numberFormatter.format(number);
+ }
+ public static void observeProfile(LifecycleOwner lifecycleOwner, Observer<Profile> observer) {
+ profile.observe(lifecycleOwner, observer);
}
-
public static void removeProfileObservers(LifecycleOwner owner) {
profile.removeObservers(owner);
}
+ public static float parseNumber(String str) throws ParseException {
+ ParsePosition pos = new ParsePosition(0);
+ Number parsed = numberFormatter.parse(str);
+ if (parsed == null || pos.getErrorIndex() > -1)
+ throw new ParseException("Error parsing '" + str + "'", pos.getErrorIndex());
+
+ return parsed.floatValue();
+ }
}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import android.content.res.Resources;
+import android.util.SparseArray;
+
+import net.ktnx.mobileledger.R;
+
+public enum FutureDates {
+ None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
+ SixMonths(180), OneYear(365), All(-1);
+ private static final SparseArray<FutureDates> map = new SparseArray<>();
+
+ static {
+ for (FutureDates item : FutureDates.values()) {
+ map.put(item.value, item);
+ }
+ }
+
+ private final int value;
+ FutureDates(int value) {
+ this.value = value;
+ }
+ public static FutureDates valueOf(int i) {
+ return map.get(i, None);
+ }
+ public int toInt() {
+ return this.value;
+ }
+ public String getText(Resources resources) {
+ switch (value) {
+ case 7:
+ return resources.getString(R.string.future_dates_7);
+ case 14:
+ return resources.getString(R.string.future_dates_14);
+ case 30:
+ return resources.getString(R.string.future_dates_30);
+ case 60:
+ return resources.getString(R.string.future_dates_60);
+ case 90:
+ return resources.getString(R.string.future_dates_90);
+ case 180:
+ return resources.getString(R.string.future_dates_180);
+ case 365:
+ return resources.getString(R.string.future_dates_365);
+ case -1:
+ return resources.getString(R.string.future_dates_all);
+ default:
+ return resources.getString(R.string.future_dates_none);
+ }
+ }
+}
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import net.ktnx.mobileledger.json.API;
+
import java.util.Locale;
public class HledgerVersion {
- private int major;
- private int minor;
- private int patch;
- private boolean isPre_1_20;
- private boolean hasPatch;
+ private final int major;
+ private final int minor;
+ private final int patch;
+ private final boolean isPre_1_20_1;
+ private final boolean hasPatch;
public HledgerVersion(int major, int minor) {
this.major = major;
this.minor = minor;
- this.isPre_1_20 = false;
+ this.patch = 0;
+ this.isPre_1_20_1 = false;
this.hasPatch = false;
}
public HledgerVersion(int major, int minor, int patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
- this.isPre_1_20 = false;
+ this.isPre_1_20_1 = false;
this.hasPatch = true;
}
- public HledgerVersion(boolean pre_1_20) {
- if (!pre_1_20)
- throw new IllegalArgumentException("pre_1_20 argument must be true");
- this.major = this.minor = 0;
- this.isPre_1_20 = true;
+ public HledgerVersion(boolean pre_1_20_1) {
+ if (!pre_1_20_1)
+ throw new IllegalArgumentException("pre_1_20_1 argument must be true");
+ this.major = this.minor = this.patch = 0;
+ this.isPre_1_20_1 = true;
this.hasPatch = false;
}
public HledgerVersion(HledgerVersion origin) {
this.major = origin.major;
this.minor = origin.minor;
- this.isPre_1_20 = origin.isPre_1_20;
+ this.isPre_1_20_1 = origin.isPre_1_20_1;
this.patch = origin.patch;
this.hasPatch = origin.hasPatch;
}
return false;
HledgerVersion that = (HledgerVersion) obj;
- return (this.isPre_1_20 == that.isPre_1_20 && this.major == that.major &&
+ return (this.isPre_1_20_1 == that.isPre_1_20_1 && this.major == that.major &&
this.minor == that.minor && this.patch == that.patch &&
this.hasPatch == that.hasPatch);
}
- public boolean isPre_1_20() {
- return isPre_1_20;
+ public boolean isPre_1_20_1() {
+ return isPre_1_20_1;
}
public int getMajor() {
return major;
@NonNull
@Override
public String toString() {
- if (isPre_1_20)
+ if (isPre_1_20_1)
return "(before 1.20)";
return hasPatch ? String.format(Locale.ROOT, "%d.%d.%d", major, minor, patch)
: String.format(Locale.ROOT, "%d.%d", major, minor);
}
+ public boolean atLeast(int major, int minor) {
+ return ((this.major == major) && (this.minor >= minor)) || (this.major > major);
+ }
+ @org.jetbrains.annotations.Nullable
+ public API getSuitableApiVersion() {
+ if (isPre_1_20_1)
+ return null;
+
+ return API.v1_19_1;
+ }
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import java.lang.ref.WeakReference;
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountValue;
+import net.ktnx.mobileledger.db.AccountWithAmounts;
+
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class LedgerAccount {
+ private static final char ACCOUNT_DELIMITER = ':';
static Pattern reHigherAccount = Pattern.compile("^[^:]+:");
+ private final LedgerAccount parent;
+ private long dbId;
+ private long profileId;
private String name;
private String shortName;
private int level;
- private final LedgerAccount parent;
private boolean expanded;
private List<LedgerAmount> amounts;
private boolean hasSubAccounts;
private boolean amountsExpanded;
- private final WeakReference<MobileLedgerProfile> profileWeakReference;
- public LedgerAccount(MobileLedgerProfile profile, String name, @Nullable LedgerAccount parent) {
- this.profileWeakReference = new WeakReference<>(profile);
+ public LedgerAccount(String name, @Nullable LedgerAccount parent) {
this.parent = parent;
if (parent != null && !name.startsWith(parent.getName() + ":"))
throw new IllegalStateException(
}
@Nullable
public static String extractParentName(@NonNull String accName) {
- int colonPos = accName.lastIndexOf(':');
+ int colonPos = accName.lastIndexOf(ACCOUNT_DELIMITER);
if (colonPos < 0)
return null; // no parent account -- this is a top-level account
else
return accName.substring(0, colonPos);
}
- public @Nullable
- MobileLedgerProfile getProfile() {
- return profileWeakReference.get();
+ public static boolean isParentOf(@NonNull String possibleParent, @NonNull String accountName) {
+ return accountName.startsWith(possibleParent + ':');
+ }
+ @NonNull
+ static public LedgerAccount fromDBO(AccountWithAmounts in, LedgerAccount parent) {
+ LedgerAccount res = new LedgerAccount(in.account.getName(), parent);
+ res.dbId = in.account.getId();
+ res.profileId = in.account.getProfileId();
+ res.setName(in.account.getName());
+ res.setExpanded(in.account.isExpanded());
+ res.setAmountsExpanded(in.account.isAmountsExpanded());
+
+ res.amounts = new ArrayList<>();
+ for (AccountValue val : in.amounts) {
+ res.amounts.add(new LedgerAmount(val.getValue(), val.getCurrency()));
+ }
+
+ return res;
+ }
+ public static int determineLevel(String accName) {
+ int level = 0;
+ int delimiterPosition = accName.indexOf(ACCOUNT_DELIMITER);
+ while (delimiterPosition >= 0) {
+ level++;
+ delimiterPosition = accName.indexOf(ACCOUNT_DELIMITER, delimiterPosition + 1);
+ }
+ return level;
}
@Override
public int hashCode() {
.startsWith(name + ":");
}
private void stripName() {
- if (parent == null) {
- level = 0;
- shortName = name;
- }
- else {
- level = parent.level + 1;
- shortName = name.substring(parent.getName()
- .length() + 1);
- }
+ String[] split = name.split(":");
+ shortName = split[split.length - 1];
+ level = split.length - 1;
}
public String getName() {
return name;
public int getLevel() {
return level;
}
-
@NonNull
public String getShortName() {
return shortName;
}
-
public String getParentName() {
return (parent == null) ? null : parent.getName();
}
if (amounts != null)
amounts.clear();
}
- public boolean amountsExpanded() { return amountsExpanded; }
- public void setAmountsExpanded(boolean flag) { amountsExpanded = flag; }
- public void toggleAmountsExpanded() { amountsExpanded = !amountsExpanded; }
-
+ public boolean amountsExpanded() {return amountsExpanded;}
+ public void setAmountsExpanded(boolean flag) {amountsExpanded = flag;}
+ public void toggleAmountsExpanded() {amountsExpanded = !amountsExpanded;}
public void propagateAmountsTo(LedgerAccount acc) {
for (LedgerAmount a : amounts)
a.propagateToAccount(acc);
}
+ public boolean allAmountsAreZero() {
+ for (LedgerAmount a : amounts) {
+ if (a.getAmount() != 0)
+ return false;
+ }
+
+ return true;
+ }
public List<LedgerAmount> getAmounts() {
return amounts;
}
+ @NonNull
+ public Account toDBO() {
+ Account dbo = new Account();
+ dbo.setName(name);
+ dbo.setNameUpper(name.toUpperCase());
+ dbo.setParentName(extractParentName(name));
+ dbo.setLevel(level);
+ dbo.setId(dbId);
+ dbo.setProfileId(profileId);
+ dbo.setExpanded(expanded);
+ dbo.setAmountsExpanded(amountsExpanded);
+
+ return dbo;
+ }
+ @NonNull
+ public AccountWithAmounts toDBOWithAmounts() {
+ AccountWithAmounts dbo = new AccountWithAmounts();
+ dbo.account = toDBO();
+
+ dbo.amounts = new ArrayList<>();
+ for (LedgerAmount amt : getAmounts()) {
+ AccountValue val = new AccountValue();
+ val.setCurrency(amt.getCurrency());
+ val.setValue(amt.getAmount());
+ dbo.amounts.add(val);
+ }
+
+ return dbo;
+ }
+ public long getId() {
+ return dbId;
+ }
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import androidx.annotation.NonNull;
+import net.ktnx.mobileledger.dao.AccountValueDAO;
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.AccountValue;
+import net.ktnx.mobileledger.db.DB;
+
public class LedgerAmount {
private final String currency;
private final float amount;
+ private long dbId;
public LedgerAmount(float amount, @NonNull String currency) {
this.currency = currency;
this.amount = amount;
}
-
public LedgerAmount(float amount) {
this.amount = amount;
this.currency = null;
}
+ static public LedgerAmount fromDBO(AccountValue dbo) {
+ final LedgerAmount ledgerAmount = new LedgerAmount(dbo.getValue(), dbo.getCurrency());
+ ledgerAmount.dbId = dbo.getId();
+ return ledgerAmount;
+ }
+ public AccountValue toDBO(Account account) {
+ final AccountValueDAO dao = DB.get()
+ .getAccountValueDAO();
+ AccountValue obj = new AccountValue();
+ obj.setId(dbId);
+ obj.setAccountId(account.getId());
+ obj.setCurrency(currency);
+ obj.setValue(amount);
+
+ return obj;
+ }
@SuppressLint("DefaultLocale")
@NonNull
public String toString() {
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.model;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.Transaction;
+import net.ktnx.mobileledger.db.TransactionAccount;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
import net.ktnx.mobileledger.utils.Digest;
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.SimpleDate;
return res;
return Float.compare(o1.getAmount(), o2.getAmount());
};
- private final String profile;
- private final Integer id;
+ private final long profile;
+ private final long ledgerId;
+ private final List<LedgerTransactionAccount> accounts;
+ private long dbId;
private SimpleDate date;
private String description;
private String comment;
- private final List<LedgerTransactionAccount> accounts;
private String dataHash;
private boolean dataLoaded;
- public LedgerTransaction(Integer id, String dateString, String description)
+ public LedgerTransaction(long ledgerId, String dateString, String description)
throws ParseException {
- this(id, Globals.parseLedgerDate(dateString), description);
+ this(ledgerId, Globals.parseLedgerDate(dateString), description);
+ }
+ public LedgerTransaction(TransactionWithAccounts dbo) {
+ this(dbo.transaction.getLedgerId(), dbo.transaction.getProfileId());
+ dbId = dbo.transaction.getId();
+ date = new SimpleDate(dbo.transaction.getYear(), dbo.transaction.getMonth(),
+ dbo.transaction.getDay());
+ description = dbo.transaction.getDescription();
+ comment = dbo.transaction.getComment();
+ dataHash = dbo.transaction.getDataHash();
+ if (dbo.accounts != null)
+ for (TransactionAccount acc : dbo.accounts) {
+ accounts.add(new LedgerTransactionAccount(acc));
+ }
+ dataLoaded = true;
+ }
+ public TransactionWithAccounts toDBO() {
+ TransactionWithAccounts o = new TransactionWithAccounts();
+ o.transaction = new Transaction();
+ o.transaction.setId(dbId);
+ o.transaction.setProfileId(profile);
+ o.transaction.setLedgerId(ledgerId);
+ o.transaction.setYear(date.year);
+ o.transaction.setMonth(date.month);
+ o.transaction.setDay(date.day);
+ o.transaction.setDescription(description);
+ o.transaction.setComment(comment);
+ fillDataHash();
+ o.transaction.setDataHash(dataHash);
+
+ o.accounts = new ArrayList<>();
+ int orderNo = 1;
+ for (LedgerTransactionAccount acc : accounts) {
+ TransactionAccount a = acc.toDBO();
+ a.setOrderNo(orderNo++);
+ a.setTransactionId(dbId);
+ o.accounts.add(a);
+ }
+ return o;
}
- public LedgerTransaction(Integer id, SimpleDate date, String description,
- MobileLedgerProfile profile) {
- this.profile = profile.getUuid();
- this.id = id;
+ public LedgerTransaction(long ledgerId, SimpleDate date, String description, Profile profile) {
+ this.profile = profile.getId();
+ this.ledgerId = ledgerId;
this.date = date;
this.description = description;
this.accounts = new ArrayList<>();
this.dataHash = null;
dataLoaded = false;
}
- public LedgerTransaction(Integer id, SimpleDate date, String description) {
- this(id, date, description, Data.getProfile());
+ public LedgerTransaction(long ledgerId, SimpleDate date, String description) {
+ this(ledgerId, date, description, Data.getProfile());
}
public LedgerTransaction(SimpleDate date, String description) {
- this(null, date, description);
+ this(0, date, description);
}
- public LedgerTransaction(int id) {
- this(id, (SimpleDate) null, null);
+ public LedgerTransaction(int ledgerId) {
+ this(ledgerId, (SimpleDate) null, null);
}
- public LedgerTransaction(int id, String profileUUID) {
- this.profile = profileUUID;
- this.id = id;
+ public LedgerTransaction(long ledgerId, long profileId) {
+ this.profile = profileId;
+ this.ledgerId = ledgerId;
this.date = null;
this.description = null;
this.accounts = new ArrayList<>();
public void setComment(String comment) {
this.comment = comment;
}
- public int getId() {
- return id;
+ public long getLedgerId() {
+ return ledgerId;
}
protected void fillDataHash() {
- loadData(App.getDatabase());
if (dataHash != null)
return;
try {
StringBuilder data = new StringBuilder();
data.append("ver1");
data.append(profile);
- data.append(getId());
+ data.append(getLedgerId());
data.append('\0');
data.append(getDescription());
data.append('\0');
String.format("Unable to get instance of %s digest", DIGEST_TYPE), e);
}
}
- public synchronized void loadData(SQLiteDatabase db) {
- if (dataLoaded)
- return;
-
- try (Cursor cTr = db.rawQuery(
- "SELECT year, month, day, description, comment from transactions WHERE profile=? " +
- "AND id=?", new String[]{profile, String.valueOf(id)}))
- {
- if (cTr.moveToFirst()) {
- date = new SimpleDate(cTr.getInt(0), cTr.getInt(1), cTr.getInt(2));
- description = cTr.getString(3);
- comment = cTr.getString(4);
-
- accounts.clear();
-
- try (Cursor cAcc = db.rawQuery(
- "SELECT account_name, amount, currency, comment FROM " +
- "transaction_accounts WHERE profile=? AND transaction_id = ?",
- new String[]{profile, String.valueOf(id)}))
- {
- while (cAcc.moveToNext()) {
-// debug("transactions",
-// String.format("Loaded %d: %s %1.2f %s", id, cAcc.getString(0),
-// cAcc.getFloat(1), cAcc.getString(2)));
- addAccount(new LedgerTransactionAccount(cAcc.getString(0), cAcc.getFloat(1),
- cAcc.getString(2), cAcc.getString(3)));
- }
-
- finishLoading();
- }
- }
- }
-
- }
public String getDataHash() {
fillDataHash();
return dataHash;
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.model;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import net.ktnx.mobileledger.db.TransactionAccount;
import net.ktnx.mobileledger.utils.Misc;
import java.util.Locale;
private String shortAccountName;
private float amount;
private boolean amountSet = false;
+ @Nullable
private String currency;
private String comment;
private boolean amountValid = true;
+ private long dbId;
public LedgerTransactionAccount(String accountName, float amount, String currency,
String comment) {
this.setAccountName(accountName);
amountValid = origin.amountValid;
currency = origin.getCurrency();
}
+ public LedgerTransactionAccount(TransactionAccount dbo) {
+ this(dbo.getAccountName(), dbo.getAmount(), Misc.emptyIsNull(dbo.getCurrency()),
+ Misc.emptyIsNull(dbo.getComment()));
+ amountSet = true;
+ amountValid = true;
+ dbId = dbo.getId();
+ }
public String getComment() {
return comment;
}
return amountSet;
}
public boolean isAmountValid() { return amountValid; }
+ @Nullable
public String getCurrency() {
return currency;
}
return sb.toString();
}
+ public TransactionAccount toDBO() {
+ TransactionAccount dbo = new TransactionAccount();
+ dbo.setAccountName(accountName);
+ if (amountSet)
+ dbo.setAmount(amount);
+ dbo.setComment(comment);
+ dbo.setCurrency(Misc.nullIsEmpty(currency));
+ dbo.setId(dbId);
+
+ return dbo;
+ }
}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import net.ktnx.mobileledger.db.TemplateHeader;
+
+import java.util.regex.MatchResult;
+
+public class MatchedTemplate {
+ public TemplateHeader templateHead;
+ public MatchResult matchResult;
+ public MatchedTemplate(TemplateHeader templateHead, MatchResult matchResult) {
+ this.templateHead = templateHead;
+ this.matchResult = matchResult;
+ }
+}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.model;
-
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.text.TextUtils;
-import android.util.SparseArray;
-
-import androidx.annotation.Nullable;
-
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DbOpQueue;
-import net.ktnx.mobileledger.async.SendTransactionTask;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import org.jetbrains.annotations.Contract;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public final class MobileLedgerProfile {
- // N.B. when adding new fields, update the copy-constructor below
- private final String uuid;
- private String name;
- private boolean permitPosting;
- private boolean showCommentsByDefault;
- private boolean showCommodityByDefault;
- private String defaultCommodity;
- private String preferredAccountsFilter;
- private String url;
- private boolean authEnabled;
- private String authUserName;
- private String authPassword;
- private int themeHue;
- private int orderNo = -1;
- private SendTransactionTask.API apiVersion = SendTransactionTask.API.auto;
- private FutureDates futureDates = FutureDates.None;
- private boolean accountsLoaded;
- private boolean transactionsLoaded;
- private HledgerVersion detectedVersion;
- // N.B. when adding new fields, update the copy-constructor below
- transient private AccountAndTransactionListSaver accountAndTransactionListSaver;
- public MobileLedgerProfile(String uuid) {
- this.uuid = uuid;
- }
- public MobileLedgerProfile(MobileLedgerProfile origin) {
- uuid = origin.uuid;
- name = origin.name;
- permitPosting = origin.permitPosting;
- showCommentsByDefault = origin.showCommentsByDefault;
- showCommodityByDefault = origin.showCommodityByDefault;
- preferredAccountsFilter = origin.preferredAccountsFilter;
- url = origin.url;
- authEnabled = origin.authEnabled;
- authUserName = origin.authUserName;
- authPassword = origin.authPassword;
- themeHue = origin.themeHue;
- orderNo = origin.orderNo;
- futureDates = origin.futureDates;
- apiVersion = origin.apiVersion;
- defaultCommodity = origin.defaultCommodity;
- accountsLoaded = origin.accountsLoaded;
- transactionsLoaded = origin.transactionsLoaded;
- if (origin.detectedVersion != null)
- detectedVersion = new HledgerVersion(origin.detectedVersion);
- }
- // loads all profiles into Data.profiles
- // returns the profile with the given UUID
- public static MobileLedgerProfile loadAllFromDB(@Nullable String currentProfileUUID) {
- MobileLedgerProfile result = null;
- ArrayList<MobileLedgerProfile> list = new ArrayList<>();
- SQLiteDatabase db = App.getDatabase();
- try (Cursor cursor = db.rawQuery("SELECT uuid, name, url, use_authentication, auth_user, " +
- "auth_password, permit_posting, theme, order_no, " +
- "preferred_accounts_filter, future_dates, api_version, " +
- "show_commodity_by_default, default_commodity, " +
- "show_comments_by_default, detected_version_pre_1_19, " +
- "detected_version_major, detected_version_minor FROM " +
- "profiles order by order_no", null))
- {
- while (cursor.moveToNext()) {
- MobileLedgerProfile item = new MobileLedgerProfile(cursor.getString(0));
- item.setName(cursor.getString(1));
- item.setUrl(cursor.getString(2));
- item.setAuthEnabled(cursor.getInt(3) == 1);
- item.setAuthUserName(cursor.getString(4));
- item.setAuthPassword(cursor.getString(5));
- item.setPostingPermitted(cursor.getInt(6) == 1);
- item.setThemeId(cursor.getInt(7));
- item.orderNo = cursor.getInt(8);
- item.setPreferredAccountsFilter(cursor.getString(9));
- item.setFutureDates(cursor.getInt(10));
- item.setApiVersion(cursor.getInt(11));
- item.setShowCommodityByDefault(cursor.getInt(12) == 1);
- item.setDefaultCommodity(cursor.getString(13));
- item.setShowCommentsByDefault(cursor.getInt(14) == 1);
- {
- boolean pre_1_20 = cursor.getInt(15) == 1;
- int major = cursor.getInt(16);
- int minor = cursor.getInt(17);
-
- if (!pre_1_20 && major == 0 && minor == 0) {
- item.detectedVersion = null;
- }
- else if (pre_1_20) {
- item.detectedVersion = new HledgerVersion(true);
- }
- else {
- item.detectedVersion = new HledgerVersion(major, minor);
- }
- }
- list.add(item);
- if (item.getUuid()
- .equals(currentProfileUUID))
- result = item;
- }
- }
- Data.profiles.postValue(list);
- return result;
- }
- public static void storeProfilesOrder() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
- int orderNo = 0;
- for (MobileLedgerProfile p : Objects.requireNonNull(Data.profiles.getValue())) {
- db.execSQL("update profiles set order_no=? where uuid=?",
- new Object[]{orderNo, p.getUuid()});
- p.orderNo = orderNo;
- orderNo++;
- }
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- public HledgerVersion getDetectedVersion() {
- return detectedVersion;
- }
- public void setDetectedVersion(HledgerVersion detectedVersion) {
- this.detectedVersion = detectedVersion;
- }
- @Contract(value = "null -> false", pure = true)
- @Override
- public boolean equals(@Nullable Object obj) {
- if (obj == null)
- return false;
- if (obj == this)
- return true;
- if (obj.getClass() != this.getClass())
- return false;
-
- MobileLedgerProfile p = (MobileLedgerProfile) obj;
- if (!uuid.equals(p.uuid))
- return false;
- if (!name.equals(p.name))
- return false;
- if (permitPosting != p.permitPosting)
- return false;
- if (showCommentsByDefault != p.showCommentsByDefault)
- return false;
- if (showCommodityByDefault != p.showCommodityByDefault)
- return false;
- if (!Objects.equals(defaultCommodity, p.defaultCommodity))
- return false;
- if (!Objects.equals(preferredAccountsFilter, p.preferredAccountsFilter))
- return false;
- if (!Objects.equals(url, p.url))
- return false;
- if (authEnabled != p.authEnabled)
- return false;
- if (!Objects.equals(authUserName, p.authUserName))
- return false;
- if (!Objects.equals(authPassword, p.authPassword))
- return false;
- if (themeHue != p.themeHue)
- return false;
- if (apiVersion != p.apiVersion)
- return false;
- if (!Objects.equals(detectedVersion, p.detectedVersion))
- return false;
- return futureDates == p.futureDates;
- }
- public boolean getShowCommentsByDefault() {
- return showCommentsByDefault;
- }
- public void setShowCommentsByDefault(boolean newValue) {
- this.showCommentsByDefault = newValue;
- }
- public boolean getShowCommodityByDefault() {
- return showCommodityByDefault;
- }
- public void setShowCommodityByDefault(boolean showCommodityByDefault) {
- this.showCommodityByDefault = showCommodityByDefault;
- }
- public String getDefaultCommodity() {
- return defaultCommodity;
- }
- public void setDefaultCommodity(String defaultCommodity) {
- this.defaultCommodity = defaultCommodity;
- }
- public void setDefaultCommodity(CharSequence defaultCommodity) {
- if (defaultCommodity == null)
- this.defaultCommodity = null;
- else
- this.defaultCommodity = String.valueOf(defaultCommodity);
- }
- public SendTransactionTask.API getApiVersion() {
- return apiVersion;
- }
- public void setApiVersion(SendTransactionTask.API apiVersion) {
- this.apiVersion = apiVersion;
- }
- public void setApiVersion(int apiVersion) {
- this.apiVersion = SendTransactionTask.API.valueOf(apiVersion);
- }
- public FutureDates getFutureDates() {
- return futureDates;
- }
- public void setFutureDates(int anInt) {
- futureDates = FutureDates.valueOf(anInt);
- }
- public void setFutureDates(FutureDates futureDates) {
- this.futureDates = futureDates;
- }
- public String getPreferredAccountsFilter() {
- return preferredAccountsFilter;
- }
- public void setPreferredAccountsFilter(String preferredAccountsFilter) {
- this.preferredAccountsFilter = preferredAccountsFilter;
- }
- public void setPreferredAccountsFilter(CharSequence preferredAccountsFilter) {
- setPreferredAccountsFilter(String.valueOf(preferredAccountsFilter));
- }
- public boolean isPostingPermitted() {
- return permitPosting;
- }
- public void setPostingPermitted(boolean permitPosting) {
- this.permitPosting = permitPosting;
- }
- public String getUuid() {
- return uuid;
- }
- public String getName() {
- return name;
- }
- public void setName(CharSequence text) {
- setName(String.valueOf(text));
- }
- public void setName(String name) {
- this.name = name;
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(CharSequence text) {
- setUrl(String.valueOf(text));
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public boolean isAuthEnabled() {
- return authEnabled;
- }
- public void setAuthEnabled(boolean authEnabled) {
- this.authEnabled = authEnabled;
- }
- public String getAuthUserName() {
- return authUserName;
- }
- public void setAuthUserName(CharSequence text) {
- setAuthUserName(String.valueOf(text));
- }
- public void setAuthUserName(String authUserName) {
- this.authUserName = authUserName;
- }
- public String getAuthPassword() {
- return authPassword;
- }
- public void setAuthPassword(CharSequence text) {
- setAuthPassword(String.valueOf(text));
- }
- public void setAuthPassword(String authPassword) {
- this.authPassword = authPassword;
- }
- public void storeInDB() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
-// debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " +
-// "url=%s, permit_posting=%s, authEnabled=%s, " +
-// "themeHue=%d", uuid, name, url,
-// permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeHue));
- db.execSQL("REPLACE INTO profiles(uuid, name, permit_posting, url, " +
- "use_authentication, auth_user, auth_password, theme, order_no, " +
- "preferred_accounts_filter, future_dates, api_version, " +
- "show_commodity_by_default, default_commodity, show_comments_by_default," +
- "detected_version_pre_1_19, detected_version_major, " +
- "detected_version_minor) " +
- "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
- new Object[]{uuid, name, permitPosting, url, authEnabled,
- authEnabled ? authUserName : null,
- authEnabled ? authPassword : null, themeHue, orderNo,
- preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt(),
- showCommodityByDefault, defaultCommodity, showCommentsByDefault,
- (detectedVersion != null) && detectedVersion.isPre_1_20(),
- (detectedVersion == null) ? 0 : detectedVersion.getMajor(),
- (detectedVersion == null) ? 0 : detectedVersion.getMinor()
- });
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- public void storeAccount(SQLiteDatabase db, int generation, LedgerAccount acc,
- boolean storeUiFields) {
- // replace into is a bad idea because it would reset hidden to its default value
- // we like the default, but for new accounts only
- String sql = "update accounts set generation = ?";
- List<Object> params = new ArrayList<>();
- params.add(generation);
- if (storeUiFields) {
- sql += ", expanded=?";
- params.add(acc.isExpanded() ? 1 : 0);
- }
- sql += " where profile=? and name=?";
- params.add(uuid);
- params.add(acc.getName());
- db.execSQL(sql, params.toArray());
-
- db.execSQL("insert into accounts(profile, name, name_upper, parent_name, level, " +
- "expanded, generation) select ?,?,?,?,?,0,? where (select changes() = 0)",
- new Object[]{uuid, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(),
- acc.getLevel(), generation
- });
-// debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid));
- }
- public void storeAccountValue(SQLiteDatabase db, int generation, String name, String currency,
- Float amount) {
- if (!TextUtils.isEmpty(currency)) {
- boolean exists;
- try (Cursor c = db.rawQuery("select 1 from currencies where name=?",
- new String[]{currency}))
- {
- exists = c.moveToFirst();
- }
- if (!exists) {
- db.execSQL(
- "insert into currencies(id, name, position, has_gap) values((select max" +
- "(id) from currencies)+1, ?, ?, ?)", new Object[]{currency,
- Objects.requireNonNull(
- Data.currencySymbolPosition.getValue()).toString(),
- Data.currencyGap.getValue()
- });
- }
- }
-
- db.execSQL("replace into account_values(profile, account, " +
- "currency, value, generation) values(?, ?, ?, ?, ?);",
- new Object[]{uuid, name, Misc.emptyIsNull(currency), amount, generation});
- }
- public void storeTransaction(SQLiteDatabase db, int generation, LedgerTransaction tr) {
- tr.fillDataHash();
-// Logger.debug("storeTransaction", String.format(Locale.US, "ID %d", tr.getId()));
- SimpleDate d = tr.getDate();
- db.execSQL("UPDATE transactions SET year=?, month=?, day=?, description=?, comment=?, " +
- "data_hash=?, generation=? WHERE profile=? AND id=?",
- new Object[]{d.year, d.month, d.day, tr.getDescription(), tr.getComment(),
- tr.getDataHash(), generation, uuid, tr.getId()
- });
- db.execSQL("INSERT INTO transactions(profile, id, year, month, day, description, " +
- "comment, data_hash, generation) " +
- "select ?,?,?,?,?,?,?,?,? WHERE (select changes() = 0)",
- new Object[]{uuid, tr.getId(), tr.getDate().year, tr.getDate().month,
- tr.getDate().day, tr.getDescription(), tr.getComment(),
- tr.getDataHash(), generation
- });
-
- int accountOrderNo = 1;
- for (LedgerTransactionAccount item : tr.getAccounts()) {
- db.execSQL("UPDATE transaction_accounts SET account_name=?, amount=?, currency=?, " +
- "comment=?, generation=? " +
- "WHERE profile=? AND transaction_id=? AND order_no=?",
- new Object[]{item.getAccountName(), item.getAmount(),
- Misc.nullIsEmpty(item.getCurrency()), item.getComment(),
- generation, uuid, tr.getId(), accountOrderNo
- });
- db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " +
- "order_no, account_name, amount, currency, comment, generation) " +
- "select ?, ?, ?, ?, ?, ?, ?, ? WHERE (select changes() = 0)",
- new Object[]{uuid, tr.getId(), accountOrderNo, item.getAccountName(),
- item.getAmount(), Misc.nullIsEmpty(item.getCurrency()),
- item.getComment(), generation
- });
-
- accountOrderNo++;
- }
-// debug("profile", String.format("Transaction %d stored", tr.getId()));
- }
- public String getOption(String name, String default_value) {
- SQLiteDatabase db = App.getDatabase();
- try (Cursor cursor = db.rawQuery("select value from options where profile = ? and name=?",
- new String[]{uuid, name}))
- {
- if (cursor.moveToFirst()) {
- String result = cursor.getString(0);
-
- if (result == null) {
- debug("profile", "returning default value for " + name);
- result = default_value;
- }
- else
- debug("profile", String.format("option %s=%s", name, result));
-
- return result;
- }
- else
- return default_value;
- }
- catch (Exception e) {
- debug("db", "returning default value for " + name, e);
- return default_value;
- }
- }
- public long getLongOption(String name, long default_value) {
- long longResult;
- String result = getOption(name, "");
- if ((result == null) || result.isEmpty()) {
- debug("profile", String.format("Returning default value for option %s", name));
- longResult = default_value;
- }
- else {
- try {
- longResult = Long.parseLong(result);
- debug("profile", String.format("option %s=%s", name, result));
- }
- catch (Exception e) {
- debug("profile", String.format("Returning default value for option %s", name), e);
- longResult = default_value;
- }
- }
-
- return longResult;
- }
- public void setOption(String name, String value) {
- debug("profile", String.format("setting option %s=%s", name, value));
- DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
- new String[]{uuid, name, value});
- }
- public void setLongOption(String name, long value) {
- setOption(name, String.valueOf(value));
- }
- public void removeFromDB() {
- SQLiteDatabase db = App.getDatabase();
- debug("db", String.format("removing profile %s from DB", uuid));
- db.beginTransactionNonExclusive();
- try {
- Object[] uuid_param = new Object[]{uuid};
- db.execSQL("delete from transaction_accounts where profile=?", uuid_param);
- db.execSQL("delete from transactions where profile=?", uuid_param);
- db.execSQL("delete from account_values where profile=?", uuid_param);
- db.execSQL("delete from accounts where profile=?", uuid_param);
- db.execSQL("delete from options where profile=?", uuid_param);
- db.execSQL("delete from profiles where uuid=?", uuid_param);
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- public LedgerTransaction loadTransaction(int transactionId) {
- LedgerTransaction tr = new LedgerTransaction(transactionId, this.uuid);
- tr.loadData(App.getDatabase());
-
- return tr;
- }
- public int getThemeHue() {
-// debug("profile", String.format("Profile.getThemeHue() returning %d", themeHue));
- return this.themeHue;
- }
- public void setThemeHue(Object o) {
- setThemeId(Integer.parseInt(String.valueOf(o)));
- }
- public void setThemeId(int themeHue) {
-// debug("profile", String.format("Profile.setThemeHue(%d) called", themeHue));
- this.themeHue = themeHue;
- }
- public int getNextTransactionsGeneration(SQLiteDatabase db) {
- int generation = 1;
- try (Cursor c = db.rawQuery("SELECT generation FROM transactions WHERE profile=? LIMIT 1",
- new String[]{uuid}))
- {
- if (c.moveToFirst()) {
- generation = c.getInt(0) + 1;
- }
- }
- return generation;
- }
- private int getNextAccountsGeneration(SQLiteDatabase db) {
- int generation = 1;
- try (Cursor c = db.rawQuery("SELECT generation FROM accounts WHERE profile=? LIMIT 1",
- new String[]{uuid}))
- {
- if (c.moveToFirst()) {
- generation = c.getInt(0) + 1;
- }
- }
- return generation;
- }
- private void deleteNotPresentAccounts(SQLiteDatabase db, int generation) {
- Logger.debug("db/benchmark", "Deleting obsolete accounts");
- db.execSQL("DELETE FROM account_values WHERE profile=? AND generation <> ?",
- new Object[]{uuid, generation});
- db.execSQL("DELETE FROM accounts WHERE profile=? AND generation <> ?",
- new Object[]{uuid, generation});
- Logger.debug("db/benchmark", "Done deleting obsolete accounts");
- }
- private void deleteNotPresentTransactions(SQLiteDatabase db, int generation) {
- Logger.debug("db/benchmark", "Deleting obsolete transactions");
- db.execSQL("DELETE FROM transaction_accounts WHERE profile=? AND generation <> ?",
- new Object[]{uuid, generation});
- db.execSQL("DELETE FROM transactions WHERE profile=? AND generation <> ?",
- new Object[]{uuid, generation});
- Logger.debug("db/benchmark", "Done deleting obsolete transactions");
- }
- public void wipeAllData() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransaction();
- try {
- String[] pUuid = new String[]{uuid};
- db.execSQL("delete from options where profile=?", pUuid);
- db.execSQL("delete from accounts where profile=?", pUuid);
- db.execSQL("delete from account_values where profile=?", pUuid);
- db.execSQL("delete from transactions where profile=?", pUuid);
- db.execSQL("delete from transaction_accounts where profile=?", pUuid);
- db.setTransactionSuccessful();
- debug("wipe", String.format(Locale.ENGLISH, "Profile %s wiped out", pUuid[0]));
- }
- finally {
- db.endTransaction();
- }
- }
- public List<Currency> getCurrencies() {
- SQLiteDatabase db = App.getDatabase();
-
- ArrayList<Currency> result = new ArrayList<>();
-
- try (Cursor c = db.rawQuery("SELECT c.id, c.name, c.position, c.has_gap FROM currencies c",
- new String[]{}))
- {
- while (c.moveToNext()) {
- Currency currency = new Currency(c.getInt(0), c.getString(1),
- Currency.Position.valueOf(c.getString(2)), c.getInt(3) == 1);
- result.add(currency);
- }
- }
-
- return result;
- }
- Currency loadCurrencyByName(String name) {
- SQLiteDatabase db = App.getDatabase();
- Currency result = tryLoadCurrencyByName(db, name);
- if (result == null)
- throw new RuntimeException(String.format("Unable to load currency '%s'", name));
- return result;
- }
- private Currency tryLoadCurrencyByName(SQLiteDatabase db, String name) {
- try (Cursor cursor = db.rawQuery(
- "SELECT c.id, c.name, c.position, c.has_gap FROM currencies c WHERE c.name=?",
- new String[]{name}))
- {
- if (cursor.moveToFirst()) {
- return new Currency(cursor.getInt(0), cursor.getString(1),
- Currency.Position.valueOf(cursor.getString(2)), cursor.getInt(3) == 1);
- }
- return null;
- }
- }
- public void storeAccountAndTransactionListAsync(List<LedgerAccount> accounts,
- List<LedgerTransaction> transactions) {
- if (accountAndTransactionListSaver != null)
- accountAndTransactionListSaver.interrupt();
-
- accountAndTransactionListSaver =
- new AccountAndTransactionListSaver(this, accounts, transactions);
- accountAndTransactionListSaver.start();
- }
-
- public enum FutureDates {
- None(0), OneWeek(7), TwoWeeks(14), OneMonth(30), TwoMonths(60), ThreeMonths(90),
- SixMonths(180), OneYear(365), All(-1);
- private static final SparseArray<FutureDates> map = new SparseArray<>();
-
- static {
- for (FutureDates item : FutureDates.values()) {
- map.put(item.value, item);
- }
- }
-
- private int value;
- FutureDates(int value) {
- this.value = value;
- }
- public static FutureDates valueOf(int i) {
- return map.get(i, None);
- }
- public int toInt() {
- return this.value;
- }
- public String getText(Resources resources) {
- switch (value) {
- case 7:
- return resources.getString(R.string.future_dates_7);
- case 14:
- return resources.getString(R.string.future_dates_14);
- case 30:
- return resources.getString(R.string.future_dates_30);
- case 60:
- return resources.getString(R.string.future_dates_60);
- case 90:
- return resources.getString(R.string.future_dates_90);
- case 180:
- return resources.getString(R.string.future_dates_180);
- case 365:
- return resources.getString(R.string.future_dates_365);
- case -1:
- return resources.getString(R.string.future_dates_all);
- default:
- return resources.getString(R.string.future_dates_none);
- }
- }
- }
-
- private static class AccountAndTransactionListSaver extends Thread {
- private final MobileLedgerProfile profile;
- private final List<LedgerAccount> accounts;
- private final List<LedgerTransaction> transactions;
- AccountAndTransactionListSaver(MobileLedgerProfile profile, List<LedgerAccount> accounts,
- List<LedgerTransaction> transactions) {
- this.accounts = accounts;
- this.transactions = transactions;
- this.profile = profile;
- }
- public int getNextDescriptionsGeneration(SQLiteDatabase db) {
- int generation = 1;
- try (Cursor c = db.rawQuery("SELECT generation FROM description_history LIMIT 1",
- null))
- {
- if (c.moveToFirst()) {
- generation = c.getInt(0) + 1;
- }
- }
- return generation;
- }
- void deleteNotPresentDescriptions(SQLiteDatabase db, int generation) {
- Logger.debug("db/benchmark", "Deleting obsolete descriptions");
- db.execSQL("DELETE FROM description_history WHERE generation <> ?",
- new Object[]{generation});
- db.execSQL("DELETE FROM description_history WHERE generation <> ?",
- new Object[]{generation});
- Logger.debug("db/benchmark", "Done deleting obsolete descriptions");
- }
- @Override
- public void run() {
- SQLiteDatabase db = App.getDatabase();
- db.beginTransactionNonExclusive();
- try {
- int accountsGeneration = profile.getNextAccountsGeneration(db);
- if (isInterrupted())
- return;
-
- int transactionsGeneration = profile.getNextTransactionsGeneration(db);
- if (isInterrupted())
- return;
-
- for (LedgerAccount acc : accounts) {
- profile.storeAccount(db, accountsGeneration, acc, false);
- if (isInterrupted())
- return;
- for (LedgerAmount amt : acc.getAmounts()) {
- profile.storeAccountValue(db, accountsGeneration, acc.getName(),
- amt.getCurrency(), amt.getAmount());
- if (isInterrupted())
- return;
- }
- }
-
- for (LedgerTransaction tr : transactions) {
- profile.storeTransaction(db, transactionsGeneration, tr);
- if (isInterrupted())
- return;
- }
-
- profile.deleteNotPresentTransactions(db, transactionsGeneration);
- if (isInterrupted()) {
- return;
- }
- profile.deleteNotPresentAccounts(db, accountsGeneration);
- if (isInterrupted())
- return;
-
- Map<String, Boolean> unique = new HashMap<>();
-
- debug("descriptions", "Starting refresh");
- int descriptionsGeneration = getNextDescriptionsGeneration(db);
- try (Cursor c = db.rawQuery("SELECT distinct description from transactions",
- null))
- {
- while (c.moveToNext()) {
- String description = c.getString(0);
- String descriptionUpper = description.toUpperCase();
- if (unique.containsKey(descriptionUpper))
- continue;
-
- storeDescription(db, descriptionsGeneration, description, descriptionUpper);
-
- unique.put(descriptionUpper, true);
- }
- }
- deleteNotPresentDescriptions(db, descriptionsGeneration);
-
- db.setTransactionSuccessful();
- }
- finally {
- db.endTransaction();
- }
- }
- private void storeDescription(SQLiteDatabase db, int generation, String description,
- String descriptionUpper) {
- db.execSQL("UPDATE description_history SET description=?, generation=? WHERE " +
- "description_upper=?", new Object[]{description, generation, descriptionUpper
- });
- db.execSQL(
- "INSERT INTO description_history(description, description_upper, generation) " +
- "select ?,?,? WHERE (select changes() = 0)",
- new Object[]{description, descriptionUpper, generation
- });
- }
- }
-}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+
+import java.io.Serializable;
+
+public class TemplateDetailSource implements Serializable {
+ public static final DiffUtil.ItemCallback<TemplateDetailSource> DIFF_CALLBACK =
+ new DiffUtil.ItemCallback<TemplateDetailSource>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull TemplateDetailSource oldItem,
+ @NonNull TemplateDetailSource newItem) {
+ return oldItem.groupNumber == newItem.groupNumber;
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull TemplateDetailSource oldItem,
+ @NonNull TemplateDetailSource newItem) {
+ return oldItem.matchedText.equals(newItem.matchedText);
+ }
+ };
+
+ private short groupNumber;
+ private String matchedText;
+ public TemplateDetailSource() {
+ }
+ public TemplateDetailSource(short groupNumber, String matchedText) {
+ this.groupNumber = groupNumber;
+ this.matchedText = matchedText;
+ }
+ public short getGroupNumber() {
+ return groupNumber;
+ }
+ public void setGroupNumber(short groupNumber) {
+ this.groupNumber = groupNumber;
+ }
+ public String getMatchedText() {
+ return matchedText;
+ }
+ public void setMatchedText(String matchedText) {
+ this.matchedText = matchedText;
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.model;
+
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.Typeface;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateBase;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+abstract public class TemplateDetailsItem {
+ private final Type type;
+ protected Long id;
+ protected long position;
+
+ protected TemplateDetailsItem(Type type) {
+ this.type = type;
+ }
+ @Contract(" -> new")
+ public static @NotNull TemplateDetailsItem.Header createHeader() {
+ return new Header();
+ }
+ public static @NotNull TemplateDetailsItem.Header createHeader(Header origin) {
+ return new Header(origin);
+ }
+ @Contract("-> new")
+ public static @NotNull TemplateDetailsItem.AccountRow createAccountRow() {
+ return new AccountRow();
+ }
+ public static TemplateDetailsItem fromRoomObject(TemplateBase p) {
+ if (p instanceof TemplateHeader) {
+ TemplateHeader ph = (TemplateHeader) p;
+ Header header = createHeader();
+ header.setId(ph.getId());
+ header.setName(ph.getName());
+ header.setPattern(ph.getRegularExpression());
+ header.setTestText(ph.getTestText());
+
+ if (ph.getTransactionDescriptionMatchGroup() == null)
+ header.setTransactionDescription(ph.getTransactionDescription());
+ else
+ header.setTransactionDescriptionMatchGroup(
+ ph.getTransactionDescriptionMatchGroup());
+
+ if (ph.getTransactionCommentMatchGroup() == null)
+ header.setTransactionComment(ph.getTransactionComment());
+ else
+ header.setTransactionCommentMatchGroup(ph.getTransactionCommentMatchGroup());
+
+ if (ph.getDateDayMatchGroup() == null)
+ header.setDateDay(ph.getDateDay());
+ else
+ header.setDateDayMatchGroup(ph.getDateDayMatchGroup());
+
+ if (ph.getDateMonthMatchGroup() == null)
+ header.setDateMonth(ph.getDateMonth());
+ else
+ header.setDateMonthMatchGroup(ph.getDateMonthMatchGroup());
+
+ if (ph.getDateYearMatchGroup() == null)
+ header.setDateYear(ph.getDateYear());
+ else
+ header.setDateYearMatchGroup(ph.getDateYearMatchGroup());
+
+ header.setFallback(ph.isFallback());
+
+ return header;
+ }
+ else if (p instanceof TemplateAccount) {
+ TemplateAccount pa = (TemplateAccount) p;
+ AccountRow acc = createAccountRow();
+ acc.setId(pa.getId());
+ acc.setPosition(pa.getPosition());
+
+ if (pa.getAccountNameMatchGroup() == null)
+ acc.setAccountName(Misc.nullIsEmpty(pa.getAccountName()));
+ else
+ acc.setAccountNameMatchGroup(pa.getAccountNameMatchGroup());
+
+ if (pa.getAccountCommentMatchGroup() == null)
+ acc.setAccountComment(Misc.nullIsEmpty(pa.getAccountComment()));
+ else
+ acc.setAccountCommentMatchGroup(pa.getAccountCommentMatchGroup());
+
+ if (pa.getCurrencyMatchGroup() == null) {
+ acc.setCurrency(pa.getCurrencyObject());
+ }
+ else
+ acc.setCurrencyMatchGroup(pa.getCurrencyMatchGroup());
+
+ final Integer amountMatchGroup = pa.getAmountMatchGroup();
+ if (amountMatchGroup != null && amountMatchGroup > 0) {
+ acc.setAmountMatchGroup(amountMatchGroup);
+ final Boolean negateAmount = pa.getNegateAmount();
+ acc.setNegateAmount(negateAmount != null && negateAmount);
+ }
+ else
+ acc.setAmount(pa.getAmount());
+
+ return acc;
+ }
+ else {
+ throw new IllegalStateException("Unexpected item class " + p.getClass());
+ }
+ }
+ public Header asHeaderItem() {
+ ensureType(Type.HEADER);
+ return (Header) this;
+ }
+ public AccountRow asAccountRowItem() {
+ ensureType(Type.ACCOUNT_ITEM);
+ return (AccountRow) this;
+ }
+ private void ensureType(Type type) {
+ if (this.type != type)
+ throw new IllegalStateException(
+ String.format("Type is %s, but %s is required", this.type.toString(),
+ type.toString()));
+ }
+ void ensureTrue(boolean flag) {
+ if (!flag)
+ throw new IllegalStateException(
+ "Literal value requested, but it is matched via a pattern group");
+ }
+ void ensureFalse(boolean flag) {
+ if (flag)
+ throw new IllegalStateException("Matching group requested, but the value is a literal");
+ }
+ public long getId() {
+ return id;
+ }
+ public void setId(Long id) {
+ this.id = id;
+ }
+ public void setId(int id) {
+ this.id = (long) id;
+ }
+ public long getPosition() {
+ return position;
+ }
+ public void setPosition(long position) {
+ this.position = position;
+ }
+ abstract public String getProblem(@NonNull Resources r, int patternGroupCount);
+ public Type getType() {
+ return type;
+ }
+ public enum Type {
+ HEADER(TYPE.header), ACCOUNT_ITEM(TYPE.accountItem);
+ final int index;
+ Type(int i) {
+ index = i;
+ }
+ public int toInt() {
+ return index;
+ }
+ }
+
+ static class PossiblyMatchedValue<T> {
+ private boolean literalValue;
+ private T value;
+ private int matchGroup;
+ public PossiblyMatchedValue() {
+ literalValue = true;
+ value = null;
+ }
+ public PossiblyMatchedValue(@NonNull PossiblyMatchedValue<T> origin) {
+ literalValue = origin.literalValue;
+ value = origin.value;
+ matchGroup = origin.matchGroup;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<Integer> withLiteralInt(Integer initialValue) {
+ PossiblyMatchedValue<Integer> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<Float> withLiteralFloat(Float initialValue) {
+ PossiblyMatchedValue<Float> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ public static PossiblyMatchedValue<Short> withLiteralShort(Short initialValue) {
+ PossiblyMatchedValue<Short> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ @NonNull
+ public static PossiblyMatchedValue<String> withLiteralString(String initialValue) {
+ PossiblyMatchedValue<String> result = new PossiblyMatchedValue<>();
+ result.setValue(initialValue);
+ return result;
+ }
+ public void copyFrom(@NonNull PossiblyMatchedValue<T> origin) {
+ literalValue = origin.literalValue;
+ value = origin.value;
+ matchGroup = origin.matchGroup;
+ }
+ public T getValue() {
+ if (!literalValue)
+ throw new IllegalStateException("Value is not literal");
+ return value;
+ }
+ public void setValue(T newValue) {
+ value = newValue;
+ literalValue = true;
+ }
+ public boolean hasLiteralValue() {
+ return literalValue;
+ }
+ public int getMatchGroup() {
+ if (literalValue)
+ throw new IllegalStateException("Value is literal");
+ return matchGroup;
+ }
+ public void setMatchGroup(int group) {
+ this.matchGroup = group;
+ literalValue = false;
+ }
+ public boolean equals(PossiblyMatchedValue<T> other) {
+ if (!other.literalValue == literalValue)
+ return false;
+ if (literalValue) {
+ if (value == null)
+ return other.value == null;
+ return value.equals(other.value);
+ }
+ else
+ return matchGroup == other.matchGroup;
+ }
+ public void switchToLiteral() {
+ literalValue = true;
+ }
+ public String toString() {
+ if (literalValue)
+ if (value == null)
+ return "<null>";
+ else
+ return value.toString();
+ if (matchGroup > 0)
+ return "grp:" + matchGroup;
+ return "<null>";
+ }
+ public boolean isEmpty() {
+ if (literalValue)
+ return value == null || Misc.emptyIsNull(value.toString()) == null;
+
+ return matchGroup > 0;
+ }
+ }
+
+ public static class TYPE {
+ public static final int header = 0;
+ public static final int accountItem = 1;
+ }
+
+ public static class AccountRow extends TemplateDetailsItem {
+ private final PossiblyMatchedValue<String> accountName =
+ PossiblyMatchedValue.withLiteralString("");
+ private final PossiblyMatchedValue<String> accountComment =
+ PossiblyMatchedValue.withLiteralString("");
+ private final PossiblyMatchedValue<Float> amount =
+ PossiblyMatchedValue.withLiteralFloat(null);
+ private final PossiblyMatchedValue<net.ktnx.mobileledger.db.Currency> currency =
+ new PossiblyMatchedValue<>();
+ private boolean negateAmount;
+ public AccountRow() {
+ super(Type.ACCOUNT_ITEM);
+ }
+ public AccountRow(AccountRow origin) {
+ super(Type.ACCOUNT_ITEM);
+ id = origin.id;
+ position = origin.position;
+ accountName.copyFrom(origin.accountName);
+ accountComment.copyFrom(origin.accountComment);
+ amount.copyFrom(origin.amount);
+ currency.copyFrom(origin.currency);
+ negateAmount = origin.negateAmount;
+ }
+ public boolean isNegateAmount() {
+ return negateAmount;
+ }
+ public void setNegateAmount(boolean negateAmount) {
+ this.negateAmount = negateAmount;
+ }
+ public int getAccountCommentMatchGroup() {
+ return accountComment.getMatchGroup();
+ }
+ public void setAccountCommentMatchGroup(int group) {
+ accountComment.setMatchGroup(group);
+ }
+ public String getAccountComment() {
+ return accountComment.getValue();
+ }
+ public void setAccountComment(String comment) {
+ this.accountComment.setValue(comment);
+ }
+ public int getCurrencyMatchGroup() {
+ return currency.getMatchGroup();
+ }
+ public void setCurrencyMatchGroup(int group) {
+ currency.setMatchGroup(group);
+ }
+ public net.ktnx.mobileledger.db.Currency getCurrency() {
+ return currency.getValue();
+ }
+ public void setCurrency(net.ktnx.mobileledger.db.Currency currency) {
+ this.currency.setValue(currency);
+ }
+ public int getAccountNameMatchGroup() {
+ return accountName.getMatchGroup();
+ }
+ public void setAccountNameMatchGroup(int group) {
+ accountName.setMatchGroup(group);
+ }
+ public String getAccountName() {
+ return accountName.getValue();
+ }
+ public void setAccountName(String accountName) {
+ this.accountName.setValue(accountName);
+ }
+ public boolean hasLiteralAccountName() { return accountName.hasLiteralValue(); }
+ public boolean hasLiteralAmount() {
+ return amount.hasLiteralValue();
+ }
+ public int getAmountMatchGroup() {
+ return amount.getMatchGroup();
+ }
+ public void setAmountMatchGroup(int group) {
+ amount.setMatchGroup(group);
+ }
+ public Float getAmount() {
+ return amount.getValue();
+ }
+ public void setAmount(Float amount) {
+ this.amount.setValue(amount);
+ }
+ public String getProblem(@NonNull Resources r, int patternGroupCount) {
+ if (Misc.emptyIsNull(accountName.getValue()) == null)
+ return r.getString(R.string.account_name_is_empty);
+ if (!amount.hasLiteralValue() &&
+ (amount.getMatchGroup() < 1 || amount.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ return null;
+ }
+ public boolean hasLiteralAccountComment() {
+ return accountComment.hasLiteralValue();
+ }
+ public boolean hasLiteralCurrency() { return currency.hasLiteralValue(); }
+ public boolean equalContents(AccountRow o) {
+ if (position != o.position) {
+ Logger.debug("cmpAcc",
+ String.format(Locale.US, "[%d] != [%d]: pos %d != pos %d", getId(),
+ o.getId(), position, o.position));
+ return false;
+ }
+ return amount.equals(o.amount) && accountName.equals(o.accountName) &&
+ position == o.position && accountComment.equals(o.accountComment) &&
+ negateAmount == o.negateAmount;
+ }
+ public void switchToLiteralAmount() {
+ amount.switchToLiteral();
+ }
+ public void switchToLiteralCurrency() {
+ currency.switchToLiteral();
+ }
+ public void switchToLiteralAccountName() {
+ accountName.switchToLiteral();
+ }
+ public void switchToLiteralAccountComment() {
+ accountComment.switchToLiteral();
+ }
+ public TemplateAccount toDBO(@NonNull Long patternId) {
+ TemplateAccount result = new TemplateAccount(id, patternId, position);
+
+ if (accountName.hasLiteralValue())
+ result.setAccountName(accountName.getValue());
+ else
+ result.setAccountNameMatchGroup(accountName.getMatchGroup());
+
+ if (accountComment.hasLiteralValue())
+ result.setAccountComment(accountComment.getValue());
+ else
+ result.setAccountCommentMatchGroup(accountComment.getMatchGroup());
+
+ if (amount.hasLiteralValue()) {
+ result.setAmount(amount.getValue());
+ result.setNegateAmount(null);
+ }
+ else {
+ result.setAmountMatchGroup(amount.getMatchGroup());
+ result.setNegateAmount(negateAmount ? true : null);
+ }
+
+ if (currency.hasLiteralValue()) {
+ net.ktnx.mobileledger.db.Currency c = currency.getValue();
+ result.setCurrency((c == null) ? null : c.getId());
+ }
+ else {
+ result.setCurrencyMatchGroup(currency.getMatchGroup());
+ }
+
+ return result;
+ }
+ public boolean isEmpty() {
+ return accountName.isEmpty() && accountComment.isEmpty() && amount.isEmpty();
+ }
+ }
+
+ public static class Header extends TemplateDetailsItem {
+ private String pattern = "";
+ private String testText = "";
+ private String name = "";
+ private Pattern compiledPattern;
+ private String patternError;
+ private PossiblyMatchedValue<String> transactionDescription =
+ PossiblyMatchedValue.withLiteralString("");
+ private PossiblyMatchedValue<String> transactionComment =
+ PossiblyMatchedValue.withLiteralString("");
+ private PossiblyMatchedValue<Integer> dateYear = PossiblyMatchedValue.withLiteralInt(null);
+ private PossiblyMatchedValue<Integer> dateMonth = PossiblyMatchedValue.withLiteralInt(null);
+ private PossiblyMatchedValue<Integer> dateDay = PossiblyMatchedValue.withLiteralInt(null);
+ private SpannableString testMatch;
+ private boolean isFallback;
+ private Header() {
+ super(Type.HEADER);
+ }
+ public Header(Header origin) {
+ this();
+ id = origin.id;
+ name = origin.name;
+ testText = origin.testText;
+ testMatch = origin.testMatch;
+ setPattern(origin.pattern);
+
+ transactionDescription = new PossiblyMatchedValue<>(origin.transactionDescription);
+ transactionComment = new PossiblyMatchedValue<>(origin.transactionComment);
+
+ dateYear = new PossiblyMatchedValue<>(origin.dateYear);
+ dateMonth = new PossiblyMatchedValue<>(origin.dateMonth);
+ dateDay = new PossiblyMatchedValue<>(origin.dateDay);
+
+ isFallback = origin.isFallback;
+ }
+ private static StyleSpan capturedSpan() { return new StyleSpan(Typeface.BOLD); }
+ private static UnderlineSpan matchedSpan() { return new UnderlineSpan(); }
+ private static ForegroundColorSpan notMatchedSpan() {
+ return new ForegroundColorSpan(Color.GRAY);
+ }
+ public boolean isFallback() {
+ return isFallback;
+ }
+ public void setFallback(boolean fallback) {
+ this.isFallback = fallback;
+ }
+ public String getName() {
+ return name;
+ }
+ public void setName(String name) {
+ this.name = name;
+ }
+ public String getPattern() {
+ return pattern;
+ }
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ try {
+ this.compiledPattern = Pattern.compile(pattern);
+ checkPatternMatch();
+ }
+ catch (PatternSyntaxException ex) {
+ patternError = ex.getDescription();
+ compiledPattern = null;
+
+ testMatch = new SpannableString(testText);
+ if (!testText.isEmpty())
+ testMatch.setSpan(notMatchedSpan(), 0, testText.length() - 1,
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ return super.toString() +
+ String.format(" name[%s] pat[%s] test[%s] tran[%s] com[%s]", name, pattern,
+ testText, transactionDescription, transactionComment);
+ }
+ public String getTestText() {
+ return testText;
+ }
+ public void setTestText(String testText) {
+ this.testText = testText;
+
+ checkPatternMatch();
+ }
+ public String getTransactionDescription() {
+ return transactionDescription.getValue();
+ }
+ public void setTransactionDescription(String transactionDescription) {
+ this.transactionDescription.setValue(transactionDescription);
+ }
+ public String getTransactionComment() {
+ return transactionComment.getValue();
+ }
+ public void setTransactionComment(String transactionComment) {
+ this.transactionComment.setValue(transactionComment);
+ }
+ public Integer getDateYear() {
+ return dateYear.getValue();
+ }
+ public void setDateYear(Integer dateYear) {
+ this.dateYear.setValue(dateYear);
+ }
+ public Integer getDateMonth() {
+ return dateMonth.getValue();
+ }
+ public void setDateMonth(Integer dateMonth) {
+ this.dateMonth.setValue(dateMonth);
+ }
+ public Integer getDateDay() {
+ return dateDay.getValue();
+ }
+ public void setDateDay(Integer dateDay) {
+ this.dateDay.setValue(dateDay);
+ }
+ public int getDateYearMatchGroup() {
+ return dateYear.getMatchGroup();
+ }
+ public void setDateYearMatchGroup(int dateYearMatchGroup) {
+ this.dateYear.setMatchGroup(dateYearMatchGroup);
+ }
+ public int getDateMonthMatchGroup() {
+ return dateMonth.getMatchGroup();
+ }
+ public void setDateMonthMatchGroup(int dateMonthMatchGroup) {
+ this.dateMonth.setMatchGroup(dateMonthMatchGroup);
+ }
+ public int getDateDayMatchGroup() {
+ return dateDay.getMatchGroup();
+ }
+ public void setDateDayMatchGroup(int dateDayMatchGroup) {
+ this.dateDay.setMatchGroup(dateDayMatchGroup);
+ }
+ public boolean hasLiteralDateYear() {
+ return dateYear.hasLiteralValue();
+ }
+ public boolean hasLiteralDateMonth() {
+ return dateMonth.hasLiteralValue();
+ }
+ public boolean hasLiteralDateDay() {
+ return dateDay.hasLiteralValue();
+ }
+ public boolean hasLiteralTransactionDescription() { return transactionDescription.hasLiteralValue(); }
+ public boolean hasLiteralTransactionComment() { return transactionComment.hasLiteralValue(); }
+ public String getProblem(@NonNull Resources r, int patternGroupCount) {
+ if (patternError != null)
+ return r.getString(R.string.pattern_has_errors) + ": " + patternError;
+ if (Misc.emptyIsNull(pattern) == null)
+ return r.getString(R.string.pattern_is_empty);
+
+ if (!dateYear.hasLiteralValue() && compiledPattern != null &&
+ (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ if (!dateMonth.hasLiteralValue() && compiledPattern != null &&
+ (dateMonth.getMatchGroup() < 1 || dateMonth.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ if (!dateDay.hasLiteralValue() && compiledPattern != null &&
+ (dateDay.getMatchGroup() < 1 || dateDay.getMatchGroup() > patternGroupCount))
+ return r.getString(R.string.invalid_matching_group_number);
+
+ return null;
+ }
+
+ public boolean equalContents(Header o) {
+ if (!dateDay.equals(o.dateDay))
+ return false;
+ if (!dateMonth.equals(o.dateMonth))
+ return false;
+ if (!dateYear.equals(o.dateYear))
+ return false;
+ if (!transactionDescription.equals(o.transactionDescription))
+ return false;
+ if (!transactionComment.equals(o.transactionComment))
+ return true;
+
+ return Misc.equalStrings(name, o.name) && Misc.equalStrings(pattern, o.pattern) &&
+ Misc.equalStrings(testText, o.testText) &&
+ Misc.equalStrings(patternError, o.patternError) &&
+ Objects.equals(testMatch, o.testMatch) && isFallback == o.isFallback;
+ }
+ public String getMatchGroupText(int group) {
+ if (compiledPattern != null && testText != null) {
+ Matcher m = compiledPattern.matcher(testText);
+ if (m.matches())
+ return m.group(group);
+ }
+
+ return "ø";
+ }
+ public Pattern getCompiledPattern() {
+ return compiledPattern;
+ }
+ public void switchToLiteralTransactionDescription() {
+ transactionDescription.switchToLiteral();
+ }
+ public void switchToLiteralTransactionComment() {
+ transactionComment.switchToLiteral();
+ }
+ public int getTransactionDescriptionMatchGroup() {
+ return transactionDescription.getMatchGroup();
+ }
+ public void setTransactionDescriptionMatchGroup(int group) {
+ transactionDescription.setMatchGroup(group);
+ }
+ public int getTransactionCommentMatchGroup() {
+ return transactionComment.getMatchGroup();
+ }
+ public void setTransactionCommentMatchGroup(int group) {
+ transactionComment.setMatchGroup(group);
+ }
+ public void switchToLiteralDateYear() {
+ dateYear.switchToLiteral();
+ }
+ public void switchToLiteralDateMonth() {
+ dateMonth.switchToLiteral();
+ }
+ public void switchToLiteralDateDay() { dateDay.switchToLiteral(); }
+ public TemplateHeader toDBO() {
+ TemplateHeader result = new TemplateHeader(id, name, pattern);
+
+ if (Misc.emptyIsNull(testText) != null)
+ result.setTestText(testText);
+
+ if (transactionDescription.hasLiteralValue())
+ result.setTransactionDescription(transactionDescription.getValue());
+ else
+ result.setTransactionDescriptionMatchGroup(transactionDescription.getMatchGroup());
+
+ if (transactionComment.hasLiteralValue())
+ result.setTransactionComment(transactionComment.getValue());
+ else
+ result.setTransactionCommentMatchGroup(transactionComment.getMatchGroup());
+
+ if (dateYear.hasLiteralValue())
+ result.setDateYear(dateYear.getValue());
+ else
+ result.setDateYearMatchGroup(dateYear.getMatchGroup());
+
+ if (dateMonth.hasLiteralValue())
+ result.setDateMonth(dateMonth.getValue());
+ else
+ result.setDateMonthMatchGroup(dateMonth.getMatchGroup());
+
+ if (dateDay.hasLiteralValue())
+ result.setDateDay(dateDay.getValue());
+ else
+ result.setDateDayMatchGroup(dateDay.getMatchGroup());
+
+ result.setFallback(isFallback);
+
+ return result;
+ }
+ public SpannableString getTestMatch() {
+ return testMatch;
+ }
+ public void checkPatternMatch() {
+ patternError = null;
+ testMatch = null;
+
+ if (pattern != null) {
+ try {
+ if (Misc.emptyIsNull(testText) != null) {
+ SpannableString ss = new SpannableString(testText);
+ Matcher m = compiledPattern.matcher(testText);
+ if (m.find()) {
+ if (m.start() > 0)
+ ss.setSpan(notMatchedSpan(), 0, m.start(),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ if (m.end() < testText.length() - 1)
+ ss.setSpan(notMatchedSpan(), m.end(), testText.length(),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ ss.setSpan(matchedSpan(), m.start(0), m.end(0),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+
+ if (m.groupCount() > 0) {
+ for (int g = 1; g <= m.groupCount(); g++) {
+ ss.setSpan(capturedSpan(), m.start(g), m.end(g),
+ Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+ }
+ }
+ else {
+ patternError = "Pattern does not match";
+ ss.setSpan(new ForegroundColorSpan(Color.GRAY), 0,
+ testText.length() - 1, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+ }
+
+ testMatch = ss;
+ }
+ }
+ catch (PatternSyntaxException e) {
+ this.compiledPattern = null;
+ this.patternError = e.getMessage();
+ }
+ }
+ else {
+ patternError = "Missing pattern";
+ }
+ }
+ public String getPatternError() {
+ return patternError;
+ }
+ public SpannableString testMatch() {
+ return testMatch;
+ }
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.model;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.utils.SimpleDate;
import org.jetbrains.annotations.NotNull;
private SimpleDate date;
private boolean monthShown;
private LedgerTransaction transaction;
+ private String boldAccountName;
+ private String runningTotal;
public TransactionListItem(@NotNull SimpleDate date, boolean monthShown) {
this.type = Type.DELIMITER;
this.date = date;
this.monthShown = monthShown;
}
- public TransactionListItem(@NotNull LedgerTransaction transaction) {
+ public TransactionListItem(@NotNull LedgerTransaction transaction,
+ @Nullable String boldAccountName, @Nullable String runningTotal) {
this.type = Type.TRANSACTION;
this.transaction = transaction;
+ this.boldAccountName = boldAccountName;
+ this.runningTotal = runningTotal;
}
public TransactionListItem() {
this.type = Type.HEADER;
}
+ public String getRunningTotal() {
+ return runningTotal;
+ }
@NonNull
public Type getType() {
return type;
public SimpleDate getDate() {
if (date != null)
return date;
- if (type == Type.HEADER)
- throw new IllegalStateException("Header item has no date");
- transaction.loadData(App.getDatabase());
+ if (type != Type.TRANSACTION)
+ throw new IllegalStateException("Only transaction items have a date");
return transaction.getDate();
}
public boolean isMonthShown() {
String.format("Item type is not %s, but %s", Type.TRANSACTION, type));
return transaction;
}
- public enum Type {TRANSACTION, DELIMITER, HEADER}
+ public @Nullable
+ String getBoldAccountName() {
+ return boldAccountName;
+ }
+ public enum Type {
+ TRANSACTION, DELIMITER, HEADER;
+ public static Type valueOf(int i) {
+ if (i == TRANSACTION.ordinal())
+ return TRANSACTION;
+ else if (i == DELIMITER.ordinal())
+ return DELIMITER;
+ else if (i == HEADER.ordinal())
+ return HEADER;
+ else
+ throw new IllegalStateException("Unexpected value: " + i);
+ }
+ }
}
/*
- * Copyright © 2019 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import android.view.View;
import android.widget.RadioButton;
import android.widget.RadioGroup;
-import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import net.ktnx.mobileledger.App;
+import com.google.android.material.switchmaterial.SwitchMaterial;
+
import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.dao.CurrencyDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.Currency;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CopyOnWriteArrayList;
/**
* A fragment representing a list of Items.
model = new ViewModelProvider(this).get(CurrencySelectorModel.class);
if (onCurrencySelectedListener != null)
model.setOnCurrencySelectedListener(onCurrencySelectedListener);
- MobileLedgerProfile profile = Objects.requireNonNull(Data.getProfile());
+ Profile profile = Data.getProfile();
- model.currencies.setValue(new CopyOnWriteArrayList<>(profile.getCurrencies()));
CurrencySelectorRecyclerViewAdapter adapter = new CurrencySelectorRecyclerViewAdapter();
- model.currencies.observe(this, adapter::submitList);
+ DB.get()
+ .getCurrencyDAO()
+ .getAll()
+ .observe(this, list -> {
+ List<String> strings = new ArrayList<>();
+ for (net.ktnx.mobileledger.db.Currency c : list) {
+ strings.add(c.getName());
+ }
+ adapter.submitList(strings);
+ });
recyclerView.setAdapter(adapter);
adapter.setCurrencySelectedListener(this);
final TextView tvNoCurrBtn = csd.findViewById(R.id.btn_no_currency);
final TextView tvAddCurrOkBtn = csd.findViewById(R.id.btn_add_currency);
final TextView tvAddCurrBtn = csd.findViewById(R.id.btn_add_new);
+ final SwitchMaterial gap = csd.findViewById(R.id.currency_gap);
+ final RadioGroup rgPosition = csd.findViewById(R.id.position_radio_group);
tvNewCurrName.setVisibility(View.GONE);
tvAddCurrOkBtn.setVisibility(View.GONE);
});
tvAddCurrOkBtn.setOnClickListener(v -> {
-
-
String currName = String.valueOf(tvNewCurrName.getText());
if (!currName.isEmpty()) {
- List<Currency> list = new ArrayList<>(model.currencies.getValue());
- // FIXME hardcoded position and gap setting
- list.add(new Currency(profile, String.valueOf(tvNewCurrName.getText()),
- Currency.Position.after, false));
- model.currencies.setValue(list);
+ DB.get()
+ .getCurrencyDAO()
+ .insert(new net.ktnx.mobileledger.db.Currency(0,
+ String.valueOf(tvNewCurrName.getText()),
+ (rgPosition.getCheckedRadioButtonId() == R.id.currency_position_left)
+ ? "before" : "after", gap.isChecked()));
}
tvNewCurrName.setVisibility(View.GONE);
else
rbPositionRight.toggle();
- RadioGroup rgPosition = csd.findViewById(R.id.position_radio_group);
rgPosition.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.currency_position_left)
Data.currencySymbolPosition.setValue(Currency.Position.before);
Data.currencySymbolPosition.setValue(Currency.Position.after);
});
- Switch gap = csd.findViewById(R.id.currency_gap);
-
gap.setChecked(Data.currencyGap.getValue());
gap.setOnCheckedChangeListener((v, checked) -> Data.currencyGap.setValue(checked));
visible ? View.VISIBLE
: View.GONE));
- if ((savedInstanceState != null) ? savedInstanceState.getBoolean(ARG_SHOW_PARAMS,
- DEFAULT_SHOW_PARAMS) : DEFAULT_SHOW_PARAMS)
+ final boolean showParams;
+ if (getArguments() == null)
+ showParams = DEFAULT_SHOW_PARAMS;
+ else
+ showParams = getArguments().getBoolean(ARG_SHOW_PARAMS, DEFAULT_SHOW_PARAMS);
+
+ if (showParams)
model.showPositionAndPadding();
else
model.hidePositionAndPadding();
model.resetOnCurrencySelectedListener();
}
@Override
- public void onCurrencySelected(Currency item) {
+ public void onCurrencySelected(String item) {
model.triggerOnCurrencySelectedListener(item);
dismiss();
}
@Override
- public void onCurrencyLongClick(Currency item) {
- ArrayList<Currency> list = new ArrayList<>(model.currencies.getValue());
- App.getDatabase()
- .execSQL("delete from currencies where id=?", new Object[]{item.getId()});
- list.remove(item);
- model.currencies.setValue(list);
+ public void onCurrencyLongClick(String item) {
+ CurrencyDAO dao = DB.get()
+ .getCurrencyDAO();
+ dao.getByName(item)
+ .observe(this, dao::deleteSync);
}
public void showPositionAndPadding() {
deferredShowPositionAndPadding = true;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
-import net.ktnx.mobileledger.model.Currency;
-
-import java.util.ArrayList;
-import java.util.List;
-
public class CurrencySelectorModel extends ViewModel {
- public final MutableLiveData<List<Currency>> currencies;
private final MutableLiveData<Boolean> positionAndPaddingVisible = new MutableLiveData<>(true);
private OnCurrencySelectedListener selectionListener;
- public CurrencySelectorModel() {
- this.currencies = new MutableLiveData<>(new ArrayList<>());
- }
+ public CurrencySelectorModel() { }
public void showPositionAndPadding() {
positionAndPaddingVisible.postValue(true);
}
void resetOnCurrencySelectedListener() {
selectionListener = null;
}
- void triggerOnCurrencySelectedListener(Currency c) {
+ void triggerOnCurrencySelectedListener(String c) {
if (selectionListener != null)
selectionListener.onCurrencySelected(c);
}
import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
* specified {@link OnCurrencySelectedListener}.
*/
public class CurrencySelectorRecyclerViewAdapter
- extends ListAdapter<Currency, CurrencySelectorRecyclerViewAdapter.ViewHolder> {
+ extends ListAdapter<String, CurrencySelectorRecyclerViewAdapter.ViewHolder> {
+ private static final DiffUtil.ItemCallback<String> DIFF_CALLBACK =
+ new DiffUtil.ItemCallback<String>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull String oldItem, @NonNull String newItem) {
+ return oldItem.equals(newItem);
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull String oldItem,
+ @NonNull String newItem) {
+ return true;
+ }
+ };
private OnCurrencySelectedListener currencySelectedListener;
private OnCurrencyLongClickListener currencyLongClickListener;
public CurrencySelectorRecyclerViewAdapter() {
- super(Currency.DIFF_CALLBACK);
+ super(DIFF_CALLBACK);
}
@NotNull
@Override
public void resetCurrencySelectedListener() {
currencySelectedListener = null;
}
- public void notifyCurrencySelected(Currency currency) {
+ public void notifyCurrencySelected(String currency) {
if (null != currencySelectedListener)
currencySelectedListener.onCurrencySelected(currency);
}
this.currencyLongClickListener = listener;
}
public void resetCurrencyLockClickListener() { currencyLongClickListener = null; }
- private void notifyCurrencyLongClicked(Currency mItem) {
+ private void notifyCurrencyLongClicked(String mItem) {
if (null != currencyLongClickListener)
currencyLongClickListener.onCurrencyLongClick(mItem);
}
public class ViewHolder extends RecyclerView.ViewHolder {
private final TextView mNameView;
- private Currency mItem;
+ private String mItem;
ViewHolder(View view) {
super(view);
public String toString() {
return super.toString() + " '" + mNameView.getText() + "'";
}
- void bindTo(Currency item) {
+ void bindTo(String item) {
mItem = item;
- mNameView.setText(item.getName());
+ mNameView.setText(item);
}
}
}
import androidx.appcompat.app.AppCompatDialogFragment;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.model.FutureDates;
import net.ktnx.mobileledger.utils.SimpleDate;
import java.util.Calendar;
else
this.maxDate = maxDate.toDate().getTime();
}
- public void setFutureDates(MobileLedgerProfile.FutureDates futureDates) {
- if (futureDates == MobileLedgerProfile.FutureDates.All) {
+ public void setFutureDates(FutureDates futureDates) {
+ if (futureDates == FutureDates.All) {
maxDate = Long.MAX_VALUE;
}
else {
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import net.ktnx.mobileledger.utils.DimensionUtils;
+import net.ktnx.mobileledger.utils.Logger;
+
+public class FabManager {
+ private static final boolean FAB_SHOWN = true;
+ private static final boolean FAB_HIDDEN = false;
+ private static final int AUTO_SHOW_DELAY_MILLS = 4000;
+ private final FloatingActionButton fab;
+ private boolean wantedFabState = FAB_SHOWN;
+ private ViewPropertyAnimator fabSlideAnimator;
+ private int fabVerticalOffset;
+ public FabManager(FloatingActionButton fab) {
+ this.fab = fab;
+ }
+ public static void handle(FabHandler fabHandler, RecyclerView recyclerView) {
+ new ScrollFabHandler(fabHandler, recyclerView);
+ }
+ private void slideFabTo(int target, long duration, TimeInterpolator interpolator) {
+ fabSlideAnimator = fab.animate()
+ .translationY((float) target)
+ .setInterpolator(interpolator)
+ .setDuration(duration)
+ .setListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animation) {
+ fabSlideAnimator = null;
+ }
+ });
+ }
+ public void showFab() {
+ if (wantedFabState == FAB_SHOWN) {
+// Logger.debug("fab", "Ignoring request to show already visible FAB");
+ return;
+ }
+
+// b.btnAddTransaction.show();
+ if (this.fabSlideAnimator != null) {
+ this.fabSlideAnimator.cancel();
+ fab.clearAnimation();
+ }
+
+ Logger.debug("fab", "Showing FAB");
+ wantedFabState = FAB_SHOWN;
+ slideFabTo(0, 200L,
+ com.google.android.material.animation.AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
+ }
+ public void hideFab() {
+ if (wantedFabState == FAB_HIDDEN) {
+// Logger.debug("fab", "Ignoring request to hide FAB -- already hidden");
+ return;
+ }
+
+ calcVerticalFabOffset();
+
+// b.btnAddTransaction.hide();
+ if (this.fabSlideAnimator != null) {
+ this.fabSlideAnimator.cancel();
+ fab.clearAnimation();
+ }
+
+ Logger.debug("fab", "Hiding FAB");
+ wantedFabState = FAB_HIDDEN;
+ slideFabTo(fabVerticalOffset, 150L,
+ com.google.android.material.animation.AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR);
+ }
+ private void calcVerticalFabOffset() {
+ if (fabVerticalOffset > 0)
+ return;// already calculated
+ fab.measure(0, 0);
+
+ int height = fab.getMeasuredHeight();
+
+ int bottomMargin;
+
+ ViewGroup.LayoutParams layoutParams = fab.getLayoutParams();
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams)
+ bottomMargin = ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin;
+ else
+ throw new RuntimeException("Unsupported layout params " + layoutParams.getClass()
+ .getCanonicalName());
+
+ fabVerticalOffset = height + bottomMargin;
+ }
+ public interface FabHandler {
+ Context getContext();
+
+ void showManagedFab();
+
+ void hideManagedFab();
+ }
+
+ public static class ScrollFabHandler {
+ final private FabHandler fabHandler;
+ private int generation = 0;
+ @SuppressLint("ClickableViewAccessibility")
+ ScrollFabHandler(FabHandler fabHandler, RecyclerView recyclerView) {
+ this.fabHandler = fabHandler;
+ final float triggerAbsolutePixels = DimensionUtils.dp2px(fabHandler.getContext(), 20f);
+ final float triggerRelativePixels = triggerAbsolutePixels / 4f;
+ recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+// Logger.debug("touch", "Scrolled " + dy);
+ if (dy <= 0) {
+ showFab();
+ }
+ else
+ hideFab();
+
+ super.onScrolled(recyclerView, dx, dy);
+ }
+ });
+ recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener() {
+ private float absoluteAnchor = -1;
+ @Override
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv,
+ @NonNull MotionEvent e) {
+ switch (e.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ absoluteAnchor = e.getRawY();
+// Logger.debug("touch",
+// String.format(Locale.US, "Touch down at %4.2f", absoluteAnchor));
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (absoluteAnchor < 0)
+ break;
+
+ final float absoluteY = e.getRawY();
+// Logger.debug("touch", String.format(Locale.US, "Move to %4.2f",
+// absoluteY));
+
+ if (absoluteY > absoluteAnchor + triggerAbsolutePixels) {
+ // swipe down
+// Logger.debug("touch", "SHOW");
+ showFab();
+ absoluteAnchor = absoluteY;
+ }
+ else if (absoluteY < absoluteAnchor - triggerAbsolutePixels) {
+ // swipe up
+// Logger.debug("touch", "HIDE");
+ hideFab();
+ absoluteAnchor = absoluteY;
+ }
+
+ break;
+ }
+ return false;
+ }
+ });
+ }
+ private void hideFab() {
+ generation++;
+ int thisGeneration = generation;
+ fabHandler.hideManagedFab();
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ if (generation != thisGeneration)
+ return;
+
+ showFab();
+ }, AUTO_SHOW_DELAY_MILLS);
+ }
+ private void showFab() {
+ generation++;
+ fabHandler.showManagedFab();
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.URLSpan;
+import android.widget.TextView;
+
+import androidx.annotation.ArrayRes;
+import androidx.annotation.StringRes;
+
+import net.ktnx.mobileledger.R;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HelpDialog {
+ private final static Pattern MARKDOWN_LINK_PATTERN =
+ Pattern.compile("\\[([^\\[]+)]\\(([^)]*)\\)");
+ public static void show(Context context, @StringRes int title, @ArrayRes int content) {
+ AlertDialog.Builder adb = new AlertDialog.Builder(context);
+ adb.setTitle(title);
+ String message = TextUtils.join("\n\n", context.getResources()
+ .getStringArray(content));
+
+ SpannableStringBuilder richTextMessage = new SpannableStringBuilder();
+ while (true) {
+ Matcher m = MARKDOWN_LINK_PATTERN.matcher(message);
+ if (m.find()) {
+ richTextMessage.append(message.substring(0, m.start()));
+ String linkText = m.group(1);
+ assert linkText != null;
+ String linkURL = m.group(2);
+ assert linkURL != null;
+
+ if (linkText.isEmpty())
+ linkText = linkURL;
+
+ int spanStart = richTextMessage.length();
+ richTextMessage.append(linkText);
+ richTextMessage.setSpan(new URLSpan(linkURL), spanStart,
+ spanStart + linkText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ URLSpan linkSpan = new URLSpan(linkText);
+
+ message = message.substring(m.end());
+ }
+ else {
+ richTextMessage.append(message);
+ break;
+ }
+ }
+ adb.setMessage(richTextMessage);
+ adb.setPositiveButton(R.string.close_button, (dialog, buttonId) -> dialog.dismiss());
+ final AlertDialog dialog = adb.create();
+ dialog.show();
+ ((TextView) dialog.findViewById(android.R.id.message)).setMovementMethod(
+ LinkMovementMethod.getInstance());
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.text.TextUtils;
-
-import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
-import net.ktnx.mobileledger.App;
import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
import net.ktnx.mobileledger.async.TransactionAccumulator;
-import net.ktnx.mobileledger.async.UpdateTransactionsTask;
-import net.ktnx.mobileledger.model.AccountListItem;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.LockHolder;
-import net.ktnx.mobileledger.utils.Locker;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.SimpleDate;
import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
public class MainModel extends ViewModel {
public final MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> updatingFlag = new MutableLiveData<>(false);
- private final MutableLiveData<String> accountFilter = new MutableLiveData<>();
+ private final MutableLiveData<Boolean> showZeroBalanceAccounts = new MutableLiveData<>(true);
+ private final MutableLiveData<String> accountFilter = new MutableLiveData<>(null);
private final MutableLiveData<List<TransactionListItem>> displayedTransactions =
new MutableLiveData<>(new ArrayList<>());
- private final MutableLiveData<List<AccountListItem>> displayedAccounts =
- new MutableLiveData<>();
- private final Locker accountsLocker = new Locker();
private final MutableLiveData<String> updateError = new MutableLiveData<>();
- private final Map<String, LedgerAccount> accountMap = new HashMap<>();
- private MobileLedgerProfile profile;
- private List<LedgerAccount> allAccounts = new ArrayList<>();
private SimpleDate firstTransactionDate;
private SimpleDate lastTransactionDate;
transient private RetrieveTransactionsTask retrieveTransactionsTask;
- transient private Thread displayedAccountsUpdater;
- transient private AccountListLoader loader = null;
private TransactionsDisplayedFilter displayedTransactionsUpdater;
- public static ArrayList<LedgerAccount> mergeAccountListsFromWeb(List<LedgerAccount> oldList,
- List<LedgerAccount> newList) {
- LedgerAccount oldAcc, newAcc;
- ArrayList<LedgerAccount> merged = new ArrayList<>();
-
- Iterator<LedgerAccount> oldIterator = oldList.iterator();
- Iterator<LedgerAccount> newIterator = newList.iterator();
-
- while (true) {
- if (!oldIterator.hasNext()) {
- // the rest of the incoming are new
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- newIterator.forEachRemaining(merged::add);
- }
- else {
- while (newIterator.hasNext())
- merged.add(newIterator.next());
- }
- break;
- }
- oldAcc = oldIterator.next();
-
- if (!newIterator.hasNext()) {
- // no more incoming accounts. ignore the rest of the old
- break;
- }
- newAcc = newIterator.next();
-
- // ignore now missing old items
- if (oldAcc.getName()
- .compareTo(newAcc.getName()) < 0)
- continue;
-
- // add newly found items
- if (oldAcc.getName()
- .compareTo(newAcc.getName()) > 0)
- {
- merged.add(newAcc);
- continue;
- }
-
- // two items with same account names; forward-merge UI-controlled fields
- // it is important that the result list contains a new LedgerAccount instance
- // so that the change is propagated to the UI
- newAcc.setExpanded(oldAcc.isExpanded());
- newAcc.setAmountsExpanded(oldAcc.amountsExpanded());
- merged.add(newAcc);
- }
-
- return merged;
- }
- private void setLastUpdateStamp(long transactionCount) {
- debug("db", "Updating transaction value stamp");
- Date now = new Date();
- profile.setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
- Data.lastUpdateDate.postValue(now);
- }
- public void scheduleTransactionListReload() {
- UpdateTransactionsTask task = new UpdateTransactionsTask();
- task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, this);
- }
public LiveData<Boolean> getUpdatingFlag() {
return updatingFlag;
}
public LiveData<String> getUpdateError() {
return updateError;
}
- public void setProfile(MobileLedgerProfile profile) {
- stopTransactionsRetrieval();
- this.profile = profile;
- }
public LiveData<List<TransactionListItem>> getDisplayedTransactions() {
return displayedTransactions;
}
public void setFirstTransactionDate(SimpleDate earliestDate) {
this.firstTransactionDate = earliestDate;
}
+ public MutableLiveData<Boolean> getShowZeroBalanceAccounts() {return showZeroBalanceAccounts;}
public MutableLiveData<String> getAccountFilter() {
return accountFilter;
}
public void setLastTransactionDate(SimpleDate latestDate) {
this.lastTransactionDate = latestDate;
}
- private void applyTransactionFilter(List<LedgerTransaction> list) {
- final String accFilter = accountFilter.getValue();
- ArrayList<TransactionListItem> newList = new ArrayList<>();
-
- TransactionAccumulator accumulator = new TransactionAccumulator(this);
- if (TextUtils.isEmpty(accFilter))
- for (LedgerTransaction tr : list)
- newList.add(new TransactionListItem(tr));
- else
- for (LedgerTransaction tr : list)
- if (tr.hasAccountNamedLike(accFilter))
- newList.add(new TransactionListItem(tr));
-
- displayedTransactions.postValue(newList);
- }
public synchronized void scheduleTransactionListRetrieval() {
if (retrieveTransactionsTask != null) {
Logger.debug("db", "Ignoring request for transaction retrieval - already active");
return;
}
- MobileLedgerProfile profile = Data.getProfile();
+ Profile profile = Data.getProfile();
+ assert profile != null;
- retrieveTransactionsTask = new RetrieveTransactionsTask(this, profile, allAccounts);
+ retrieveTransactionsTask = new RetrieveTransactionsTask(profile);
Logger.debug("db", "Created a background transaction retrieval task");
- retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ retrieveTransactionsTask.start();
}
public synchronized void stopTransactionsRetrieval() {
if (retrieveTransactionsTask != null)
- retrieveTransactionsTask.cancel(true);
+ retrieveTransactionsTask.interrupt();
+ else
+ Data.backgroundTaskProgress.setValue(null);
}
public void transactionRetrievalDone() {
retrieveTransactionsTask = null;
}
- public synchronized Locker lockAccountsForWriting() {
- accountsLocker.lockForWriting();
- return accountsLocker;
- }
- public void mergeAccountListFromWeb(List<LedgerAccount> newList) {
-
- try (LockHolder l = accountsLocker.lockForWriting()) {
- allAccounts = mergeAccountListsFromWeb(allAccounts, newList);
- updateAccountsMap(allAccounts);
- }
- }
- public LiveData<List<AccountListItem>> getDisplayedAccounts() {
- return displayedAccounts;
- }
- synchronized public void scheduleAccountListReload() {
- Logger.debug("async-acc", "scheduleAccountListReload() enter");
- if ((loader != null) && loader.isAlive()) {
- Logger.debug("async-acc", "returning early - loader already active");
- return;
- }
-
- loader = new AccountListLoader(profile, this);
- loader.start();
- }
- public synchronized void setAndStoreAccountAndTransactionListFromWeb(
- List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
- profile.storeAccountAndTransactionListAsync(accounts, transactions);
-
- setLastUpdateStamp(transactions.size());
-
- mergeAccountListFromWeb(accounts);
- updateDisplayedAccounts();
-
- updateDisplayedTransactionsFromWeb(transactions);
- }
- synchronized public void abortAccountListReload() {
- if (loader == null)
- return;
- loader.interrupt();
- loader = null;
- }
- synchronized public void updateDisplayedAccounts() {
- if (displayedAccountsUpdater != null) {
- displayedAccountsUpdater.interrupt();
- }
- displayedAccountsUpdater = new AccountListDisplayedFilter(this, allAccounts);
- displayedAccountsUpdater.start();
- }
synchronized public void updateDisplayedTransactionsFromWeb(List<LedgerTransaction> list) {
if (displayedTransactionsUpdater != null) {
displayedTransactionsUpdater.interrupt();
displayedTransactionsUpdater = new TransactionsDisplayedFilter(this, list);
displayedTransactionsUpdater.start();
}
- public List<LedgerAccount> getAllAccounts() {
- return allAccounts;
- }
- private void updateAccountsMap(List<LedgerAccount> newAccounts) {
- accountMap.clear();
- for (LedgerAccount acc : newAccounts) {
- accountMap.put(acc.getName(), acc);
- }
- }
- @Nullable
- public LedgerAccount locateAccount(String name) {
- return accountMap.get(name);
- }
public void clearUpdateError() {
updateError.postValue(null);
}
- public void clearAccounts() { displayedAccounts.postValue(new ArrayList<>()); }
public void clearTransactions() {
displayedTransactions.setValue(new ArrayList<>());
}
- static class AccountListLoader extends Thread {
- private final MobileLedgerProfile profile;
- private final MainModel model;
- AccountListLoader(MobileLedgerProfile profile, MainModel model) {
- this.profile = profile;
- this.model = model;
- }
- @Override
- public void run() {
- Logger.debug("async-acc", "AccountListLoader::run() entered");
- String profileUUID = profile.getUuid();
- ArrayList<LedgerAccount> list = new ArrayList<>();
- HashMap<String, LedgerAccount> map = new HashMap<>();
-
- String sql = "SELECT a.name, a.expanded, a.amounts_expanded";
- sql += " from accounts a WHERE a.profile = ?";
- sql += " ORDER BY a.name";
-
- SQLiteDatabase db = App.getDatabase();
- Logger.debug("async-acc", "AccountListLoader::run() connected to DB");
- try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
- Logger.debug("async-acc", "AccountListLoader::run() executed query");
- while (cursor.moveToNext()) {
- if (isInterrupted())
- return;
-
- final String accName = cursor.getString(0);
-// debug("accounts",
-// String.format("Read account '%s' from DB [%s]", accName,
-// profileUUID));
- String parentName = LedgerAccount.extractParentName(accName);
- LedgerAccount parent;
- if (parentName != null) {
- parent = map.get(parentName);
- if (parent == null)
- throw new IllegalStateException(
- String.format("Can't load account '%s': parent '%s' not loaded",
- accName, parentName));
- parent.setHasSubAccounts(true);
- }
- else
- parent = null;
-
- LedgerAccount acc = new LedgerAccount(profile, accName, parent);
- acc.setExpanded(cursor.getInt(1) == 1);
- acc.setAmountsExpanded(cursor.getInt(2) == 1);
- acc.setHasSubAccounts(false);
-
- try (Cursor c2 = db.rawQuery(
- "SELECT value, currency FROM account_values WHERE profile = ?" + " " +
- "AND account = ?", new String[]{profileUUID, accName}))
- {
- while (c2.moveToNext()) {
- acc.addAmount(c2.getFloat(0), c2.getString(1));
- }
- }
-
- list.add(acc);
- map.put(accName, acc);
- }
- Logger.debug("async-acc", "AccountListLoader::run() query execution done");
- }
-
- if (isInterrupted())
- return;
-
- Logger.debug("async-acc", "AccountListLoader::run() posting new list");
- model.allAccounts = list;
- model.updateAccountsMap(list);
- model.updateDisplayedAccounts();
- }
- }
-
- static class AccountListDisplayedFilter extends Thread {
- private final MainModel model;
- private final List<LedgerAccount> list;
- AccountListDisplayedFilter(MainModel model, List<LedgerAccount> list) {
- this.model = model;
- this.list = list;
- }
- @Override
- public void run() {
- List<AccountListItem> newDisplayed = new ArrayList<>();
- Logger.debug("dFilter", "waiting for synchronized block");
- Logger.debug("dFilter", String.format(Locale.US,
- "entered synchronized block (about to examine %d accounts)", list.size()));
- newDisplayed.add(new AccountListItem()); // header
-
- int count = 0;
- for (LedgerAccount a : list) {
- if (isInterrupted())
- return;
-
- if (a.isVisible()) {
- newDisplayed.add(new AccountListItem(a));
- count++;
- }
- }
- if (!isInterrupted()) {
- model.displayedAccounts.postValue(newDisplayed);
- Data.lastUpdateAccountCount.postValue(count);
- }
- Logger.debug("dFilter", "left synchronized block");
- }
- }
-
static class TransactionsDisplayedFilter extends Thread {
private final MainModel model;
private final List<LedgerTransaction> list;
@Override
public void run() {
List<LedgerAccount> newDisplayed = new ArrayList<>();
- Logger.debug("dFilter", "waiting for synchronized block");
Logger.debug("dFilter", String.format(Locale.US,
"entered synchronized block (about to examine %d transactions)", list.size()));
String accNameFilter = model.getAccountFilter()
.getValue();
- TransactionAccumulator acc = new TransactionAccumulator(model);
+ TransactionAccumulator acc = new TransactionAccumulator(accNameFilter, accNameFilter);
for (LedgerTransaction tr : list) {
if (isInterrupted()) {
return;
acc.put(tr, tr.getDate());
}
}
- if (!isInterrupted()) {
- acc.done();
- }
- Logger.debug("dFilter", "left synchronized block");
+
+ if (isInterrupted())
+ return;
+
+ acc.publishResults(model);
+ Logger.debug("dFilter", "transaction list updated");
}
}
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui;
-import android.view.MotionEvent;
-
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.ui.transaction_list.TransactionListAdapter;
import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.DimensionUtils;
-public class MobileLedgerListFragment extends Fragment {
- public SwipeRefreshLayout refreshLayout;
+public abstract class MobileLedgerListFragment extends Fragment {
public TransactionListAdapter modelAdapter;
- protected RecyclerView root;
+ public abstract SwipeRefreshLayout getRefreshLayout();
@NonNull
public MainActivity getMainActivity() {
return (MainActivity) requireActivity();
}
protected void themeChanged(Integer counter) {
- refreshLayout.setColorSchemeColors(Colors.getSwipeCircleColors());
+ getRefreshLayout().setColorSchemeColors(Colors.getSwipeCircleColors());
}
public void onBackgroundTaskRunningChanged(Boolean isRunning) {
if (getActivity() == null)
return;
- if (refreshLayout == null)
+ SwipeRefreshLayout l = getRefreshLayout();
+ if (l == null)
return;
- refreshLayout.setRefreshing(isRunning);
- }
- protected void manageFabOnScroll() {
- final MainActivity mainActivity = getMainActivity();
- int triggerPixels = DimensionUtils.dp2px(mainActivity, 30f);
- root.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
- private float upAnchor = -1;
- private float lastY;
- @Override
- public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
- switch (e.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- lastY = upAnchor = e.getAxisValue(MotionEvent.AXIS_Y);
- break;
- case MotionEvent.ACTION_MOVE:
- final float currentY = e.getAxisValue(MotionEvent.AXIS_Y);
- if (currentY > lastY) {
- // swipe down
- upAnchor = lastY;
-
- mainActivity.fabShouldShow();
- }
- else {
- // swipe up
- if (currentY < upAnchor - triggerPixels)
- mainActivity.fabHide();
- }
-
- lastY = currentY;
-
- break;
- }
- return false;
- }
- @Override
- public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
- }
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- }
- });
+ l.setRefreshing(isRunning);
}
}
package net.ktnx.mobileledger.ui;
-import net.ktnx.mobileledger.model.Currency;
-
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnCurrencyLongClickListener {
- void onCurrencyLongClick(Currency item);
+ void onCurrencyLongClick(String item);
}
package net.ktnx.mobileledger.ui;
-import net.ktnx.mobileledger.model.Currency;
-
/**
* This interface must be implemented by activities that contain this
* fragment to allow an interaction in this fragment to be communicated
* >Communicating with Other Fragments</a> for more information.
*/
public interface OnCurrencySelectedListener {
- void onCurrencySelected(Currency item);
+ void onCurrencySelected(String item);
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+public interface OnSourceSelectedListener {
+ void onSourceSelected(boolean literal, short group);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.activity.result.ActivityResultCaller;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContract;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class QR {
+ private static final String SCAN_APP_NAME = "com.google.zxing.client.android.SCAN";
+ public static ActivityResultLauncher<Void> registerLauncher(ActivityResultCaller activity,
+ QRScanResultReceiver resultReceiver) {
+ return activity.registerForActivityResult(new ActivityResultContract<Void, String>() {
+ @NonNull
+ @Override
+ public Intent createIntent(@NonNull Context context, Void input) {
+ final Intent intent = new Intent(SCAN_APP_NAME);
+ intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
+ return intent;
+ }
+ @Override
+ public String parseResult(int resultCode, @Nullable Intent intent) {
+ if (resultCode == Activity.RESULT_CANCELED || intent == null)
+ return null;
+ return intent.getStringExtra("SCAN_RESULT");
+ }
+ }, resultReceiver::onQRScanResult);
+ }
+ public interface QRScanResultReceiver {
+ void onQRScanResult(String scanned);
+ }
+
+ public interface QRScanTrigger {
+ void triggerQRScan();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.content.Context;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.MutableLiveData;
+
+public abstract class QRScanCapableFragment extends Fragment {
+ private static final MutableLiveData<Integer> qrScanTrigger = new MutableLiveData<>();
+ protected final ActivityResultLauncher<Void> scanQrLauncher = QR.registerLauncher(this, this::onQrScanned);
+ public static void triggerQRScan() {
+ qrScanTrigger.setValue(1);
+ }
+ protected abstract void onQrScanned(String text);
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ qrScanTrigger.observe(this, ignored -> {
+ scanQrLauncher.launch(null);
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AppCompatDialogFragment;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.FragmentTemplateDetailSourceSelectorListBinding;
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A fragment representing a list of Items.
+ * <p/>
+ * Activities containing this fragment MUST implement the {@link OnSourceSelectedListener}
+ * interface.
+ */
+public class TemplateDetailSourceSelectorFragment extends AppCompatDialogFragment
+ implements OnSourceSelectedListener {
+
+ public static final int DEFAULT_COLUMN_COUNT = 1;
+ public static final String ARG_COLUMN_COUNT = "column-count";
+ public static final String ARG_PATTERN = "pattern";
+ public static final String ARG_TEST_TEXT = "test-text";
+ private int mColumnCount = DEFAULT_COLUMN_COUNT;
+ private ArrayList<TemplateDetailSource> mSources;
+ private TemplateDetailSourceSelectorModel model;
+ private OnSourceSelectedListener onSourceSelectedListener;
+ private @StringRes
+ int mPatternProblem;
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public TemplateDetailSourceSelectorFragment() {
+ }
+ @SuppressWarnings("unused")
+ public static TemplateDetailSourceSelectorFragment newInstance() {
+ return newInstance(DEFAULT_COLUMN_COUNT, null, null);
+ }
+ public static TemplateDetailSourceSelectorFragment newInstance(int columnCount,
+ @Nullable String pattern,
+ @Nullable String testText) {
+ TemplateDetailSourceSelectorFragment fragment = new TemplateDetailSourceSelectorFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_COLUMN_COUNT, columnCount);
+ if (pattern != null)
+ args.putString(ARG_PATTERN, pattern);
+ if (testText != null)
+ args.putString(ARG_TEST_TEXT, testText);
+ fragment.setArguments(args);
+ return fragment;
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getArguments() != null) {
+ mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT, DEFAULT_COLUMN_COUNT);
+ final String patternText = getArguments().getString(ARG_PATTERN);
+ final String testText = getArguments().getString(ARG_TEST_TEXT);
+ if (Misc.emptyIsNull(patternText) == null) {
+ mPatternProblem = R.string.missing_pattern_error;
+ }
+ else {
+ if (Misc.emptyIsNull(testText) == null) {
+ mPatternProblem = R.string.missing_test_text;
+ }
+ else {
+ Pattern pattern = Pattern.compile(patternText);
+ Matcher matcher = pattern.matcher(testText);
+ Logger.debug("templates",
+ String.format("Trying to match pattern '%s' against text '%s'",
+ patternText, testText));
+ if (matcher.find()) {
+ if (matcher.groupCount() >= 0) {
+ ArrayList<TemplateDetailSource> list = new ArrayList<>();
+ for (short g = 1; g <= matcher.groupCount(); g++) {
+ list.add(new TemplateDetailSource(g, matcher.group(g)));
+ }
+ mSources = list;
+ }
+ else {
+ mPatternProblem = R.string.pattern_without_groups;
+ }
+ }
+ else {
+ mPatternProblem = R.string.pattern_does_not_match;
+ }
+ }
+ }
+ }
+ }
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ Context context = requireContext();
+ Dialog csd = new Dialog(context);
+ FragmentTemplateDetailSourceSelectorListBinding b =
+ FragmentTemplateDetailSourceSelectorListBinding.inflate(
+ LayoutInflater.from(context));
+ csd.setContentView(b.getRoot());
+ csd.setTitle(R.string.choose_template_detail_source_label);
+
+ if (mSources != null && !mSources.isEmpty()) {
+ RecyclerView recyclerView = b.list;
+
+ if (mColumnCount <= 1) {
+ recyclerView.setLayoutManager(new LinearLayoutManager(context));
+ }
+ else {
+ recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+ }
+ model = new ViewModelProvider(this).get(TemplateDetailSourceSelectorModel.class);
+ if (onSourceSelectedListener != null)
+ model.setOnSourceSelectedListener(onSourceSelectedListener);
+ model.setSourcesList(mSources);
+
+ TemplateDetailSourceSelectorRecyclerViewAdapter adapter =
+ new TemplateDetailSourceSelectorRecyclerViewAdapter();
+ model.groups.observe(this, adapter::submitList);
+
+ recyclerView.setAdapter(adapter);
+ adapter.setSourceSelectedListener(this);
+ }
+ else {
+ b.list.setVisibility(View.GONE);
+ b.templateError.setText(
+ (mPatternProblem != 0) ? mPatternProblem : R.string.pattern_without_groups);
+ b.templateError.setVisibility(View.VISIBLE);
+ }
+
+ b.literalButton.setOnClickListener(v -> onSourceSelected(true, (short) -1));
+
+ return csd;
+ }
+ public void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+ onSourceSelectedListener = listener;
+
+ if (model != null)
+ model.setOnSourceSelectedListener(listener);
+ }
+ public void resetOnSourceSelectedListener() {
+ model.resetOnSourceSelectedListener();
+ }
+ @Override
+ public void onSourceSelected(boolean literal, short group) {
+ if (model != null)
+ model.triggerOnSourceSelectedListener(literal, group);
+ if (onSourceSelectedListener != null)
+ onSourceSelectedListener.onSourceSelected(literal, group);
+
+ dismiss();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TemplateDetailSourceSelectorModel extends ViewModel {
+ public final MutableLiveData<List<TemplateDetailSource>> groups = new MutableLiveData<>();
+ private OnSourceSelectedListener selectionListener;
+ public TemplateDetailSourceSelectorModel() {
+ }
+ void setOnSourceSelectedListener(OnSourceSelectedListener listener) {
+ selectionListener = listener;
+ }
+ void resetOnSourceSelectedListener() {
+ selectionListener = null;
+ }
+ void triggerOnSourceSelectedListener(boolean literal, short group) {
+ if (selectionListener != null)
+ selectionListener.onSourceSelected(literal, group);
+ }
+ public void setSourcesList(ArrayList<TemplateDetailSource> mSources) {
+ groups.setValue(mSources);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.FragmentTemplateDetailSourceSelectorBinding;
+import net.ktnx.mobileledger.model.TemplateDetailSource;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link TemplateDetailSource} and makes a call
+ * to the
+ * specified {@link OnSourceSelectedListener}.
+ */
+public class TemplateDetailSourceSelectorRecyclerViewAdapter extends
+ ListAdapter<TemplateDetailSource,
+ TemplateDetailSourceSelectorRecyclerViewAdapter.ViewHolder> {
+
+ private OnSourceSelectedListener sourceSelectedListener;
+ public TemplateDetailSourceSelectorRecyclerViewAdapter() {
+ super(TemplateDetailSource.DIFF_CALLBACK);
+ }
+ @NotNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NotNull ViewGroup parent, int viewType) {
+ FragmentTemplateDetailSourceSelectorBinding b =
+ FragmentTemplateDetailSourceSelectorBinding.inflate(
+ LayoutInflater.from(parent.getContext()), parent, false);
+ return new ViewHolder(b);
+ }
+
+ @Override
+ public void onBindViewHolder(final ViewHolder holder, int position) {
+ holder.bindTo(getItem(position));
+ }
+ public void setSourceSelectedListener(OnSourceSelectedListener listener) {
+ this.sourceSelectedListener = listener;
+ }
+ public void resetSourceSelectedListener() {
+ sourceSelectedListener = null;
+ }
+ public void notifySourceSelected(TemplateDetailSource item) {
+ if (null != sourceSelectedListener)
+ sourceSelectedListener.onSourceSelected(false, item.getGroupNumber());
+ }
+ public void notifyLiteralSelected() {
+ if (null != sourceSelectedListener)
+ sourceSelectedListener.onSourceSelected(true, (short) -1);
+ }
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ private final FragmentTemplateDetailSourceSelectorBinding b;
+ private TemplateDetailSource mItem;
+
+ ViewHolder(FragmentTemplateDetailSourceSelectorBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+
+ b.getRoot()
+ .setOnClickListener(v -> notifySourceSelected(mItem));
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return super.toString() + " " + b.groupNumber.getText() + ": '" +
+ b.matchedText.getText() + "'";
+ }
+ void bindTo(TemplateDetailSource item) {
+ mItem = item;
+ b.groupNumber.setText(String.valueOf(item.getGroupNumber()));
+ b.matchedText.setText(item.getMatchedText());
+ }
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.account_summary;
-import android.content.Context;
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
import android.content.res.Resources;
-import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DbOpQueue;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.databinding.AccountListRowBinding;
+import net.ktnx.mobileledger.databinding.AccountListSummaryRowBinding;
+import net.ktnx.mobileledger.db.Account;
+import net.ktnx.mobileledger.db.DB;
import net.ktnx.mobileledger.model.AccountListItem;
-import net.ktnx.mobileledger.model.Data;
import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.activity.MainActivity;
-import net.ktnx.mobileledger.utils.Locker;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Locale;
-import java.util.Observer;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-public class AccountSummaryAdapter
- extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
+public class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter.RowHolder> {
public static final int AMOUNT_LIMIT = 3;
+ private static final int ITEM_TYPE_HEADER = 1;
+ private static final int ITEM_TYPE_ACCOUNT = 2;
private final AsyncListDiffer<AccountListItem> listDiffer;
- private final MainModel model;
- AccountSummaryAdapter(MainModel model) {
- this.model = model;
+
+ AccountSummaryAdapter() {
+ setHasStableIds(true);
listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<AccountListItem>() {
+ @Nullable
+ @Override
+ public Object getChangePayload(@NonNull AccountListItem oldItem,
+ @NonNull AccountListItem newItem) {
+ Change changes = new Change();
+
+ final LedgerAccount oldAcc = oldItem.toAccount()
+ .getAccount();
+ final LedgerAccount newAcc = newItem.toAccount()
+ .getAccount();
+
+ if (!Misc.equalStrings(oldAcc.getName(), newAcc.getName()))
+ changes.add(Change.NAME);
+
+ if (oldAcc.getLevel() != newAcc.getLevel())
+ changes.add(Change.LEVEL);
+
+ if (oldAcc.isExpanded() != newAcc.isExpanded())
+ changes.add(Change.EXPANDED);
+
+ if (oldAcc.amountsExpanded() != newAcc.amountsExpanded())
+ changes.add(Change.EXPANDED_AMOUNTS);
+
+ if (!oldAcc.getAmountsString()
+ .equals(newAcc.getAmountsString()))
+ changes.add(Change.AMOUNTS);
+
+ return changes.toPayload();
+ }
@Override
public boolean areItemsTheSame(@NotNull AccountListItem oldItem,
@NotNull AccountListItem newItem) {
final AccountListItem.Type oldType = oldItem.getType();
final AccountListItem.Type newType = newItem.getType();
- if (oldType == AccountListItem.Type.HEADER) {
- return newType == AccountListItem.Type.HEADER;
- }
if (oldType != newType)
return false;
+ if (oldType == AccountListItem.Type.HEADER)
+ return true;
- return TextUtils.equals(oldItem.getAccount()
- .getName(), newItem.getAccount()
- .getName());
+ return oldItem.toAccount()
+ .getAccount()
+ .getId() == newItem.toAccount()
+ .getAccount()
+ .getId();
}
@Override
public boolean areContentsTheSame(@NotNull AccountListItem oldItem,
@NotNull AccountListItem newItem) {
- if (oldItem.getType()
- .equals(AccountListItem.Type.HEADER))
- return true;
- return oldItem.getAccount()
- .equals(newItem.getAccount());
+ return oldItem.sameContent(newItem);
}
});
}
-
- public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
- holder.bindToAccount(listDiffer.getCurrentList()
- .get(position));
+ @Override
+ public long getItemId(int position) {
+ if (position == 0)
+ return 0;
+ return listDiffer.getCurrentList()
+ .get(position)
+ .toAccount()
+ .getAccount()
+ .getId();
}
-
- @NonNull
@Override
- public LedgerRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- View row = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.account_summary_row, parent, false);
- return new LedgerRowHolder(row);
+ public void onBindViewHolder(@NonNull RowHolder holder, int position,
+ @NonNull List<Object> payloads) {
+ holder.bind(listDiffer.getCurrentList()
+ .get(position), payloads);
+ super.onBindViewHolder(holder, position, payloads);
}
+ public void onBindViewHolder(@NonNull RowHolder holder, int position) {
+ holder.bind(listDiffer.getCurrentList()
+ .get(position), null);
+ }
+ @NonNull
+ @Override
+ public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+ final RowHolder result;
+ switch (viewType) {
+ case ITEM_TYPE_HEADER:
+ result = new HeaderRowHolder(
+ AccountListSummaryRowBinding.inflate(inflater, parent, false));
+ break;
+ case ITEM_TYPE_ACCOUNT:
+ result = new AccountRowHolder(
+ AccountListRowBinding.inflate(inflater, parent, false));
+ break;
+ default:
+ throw new IllegalStateException("Unexpected value: " + viewType);
+ }
+// Logger.debug("acc-ui", "Creating " + result);
+ return result;
+ }
@Override
public int getItemCount() {
return listDiffer.getCurrentList()
.size();
}
+ @Override
+ public int getItemViewType(int position) {
+ return (position == 0) ? ITEM_TYPE_HEADER : ITEM_TYPE_ACCOUNT;
+ }
public void setAccounts(List<AccountListItem> newList) {
- listDiffer.submitList(newList);
+ Misc.onMainThread(() -> listDiffer.submitList(newList));
+ }
+ static class Change {
+ static final int NAME = 1;
+ static final int EXPANDED = 1 << 1;
+ static final int LEVEL = 1 << 2;
+ static final int EXPANDED_AMOUNTS = 1 << 3;
+ static final int AMOUNTS = 1 << 4;
+ private int value = 0;
+ public Change() {
+ }
+ public Change(int initialValue) {
+ value = initialValue;
+ }
+ public void add(int bits) {
+ value = value | bits;
+ }
+ public void add(Change change) {
+ value = value | change.value;
+ }
+ public void remove(int bits) {
+ value = value & (~bits);
+ }
+ public void remove(Change change) {
+ value = value & (~change.value);
+ }
+ public Change toPayload() {
+ if (value == 0)
+ return null;
+ return this;
+ }
+ public boolean has(int bits) {
+ return value == 0 || (value & bits) == bits;
+ }
}
- class LedgerRowHolder extends RecyclerView.ViewHolder {
- private final TextView tvAccountName, tvAccountAmounts;
- private final ConstraintLayout row;
- private final View expanderContainer;
- private final View amountExpanderContainer;
- private final View lLastUpdate;
- private final TextView tvLastUpdate;
- private final View vAccountNameLayout;
- LedgerAccount mAccount;
- private AccountListItem.Type lastType;
- private Observer lastUpdateObserver;
- public LedgerRowHolder(@NonNull View itemView) {
- super(itemView);
- row = itemView.findViewById(R.id.account_summary_row);
- vAccountNameLayout = itemView.findViewById(R.id.account_name_layout);
- tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
- tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
- expanderContainer = itemView.findViewById(R.id.account_expander_container);
- ImageView expander = itemView.findViewById(R.id.account_expander);
- amountExpanderContainer =
- itemView.findViewById(R.id.account_row_amounts_expander_container);
- lLastUpdate = itemView.findViewById(R.id.last_update_container);
- tvLastUpdate = itemView.findViewById(R.id.last_update_text);
+ static abstract class RowHolder extends RecyclerView.ViewHolder {
+ public RowHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ public abstract void bind(AccountListItem accountListItem, @Nullable List<Object> payloads);
+ }
- itemView.setOnLongClickListener(this::onItemLongClick);
- tvAccountName.setOnLongClickListener(this::onItemLongClick);
- tvAccountAmounts.setOnLongClickListener(this::onItemLongClick);
- expanderContainer.setOnLongClickListener(this::onItemLongClick);
- expander.setOnLongClickListener(this::onItemLongClick);
- row.setOnLongClickListener(this::onItemLongClick);
+ static class HeaderRowHolder extends RowHolder {
+ private final AccountListSummaryRowBinding b;
+ public HeaderRowHolder(@NonNull AccountListSummaryRowBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ @Override
+ public void bind(AccountListItem item, @Nullable List<Object> payloads) {
+ Resources r = itemView.getResources();
+// Logger.debug("acc", itemView.getContext()
+// .toString());
+ ((AccountListItem.Header) item).getText()
+ .observe((LifecycleOwner) itemView.getContext(),
+ b.lastUpdateText::setText);
+ }
+ }
- tvAccountName.setOnClickListener(v -> toggleAccountExpanded());
- expanderContainer.setOnClickListener(v -> toggleAccountExpanded());
- expander.setOnClickListener(v -> toggleAccountExpanded());
- tvAccountAmounts.setOnClickListener(v -> toggleAmountsExpanded());
+ class AccountRowHolder extends AccountSummaryAdapter.RowHolder {
+ private final AccountListRowBinding b;
+ public AccountRowHolder(@NonNull AccountListRowBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ itemView.setOnLongClickListener(this::onItemLongClick);
+ b.accountRowAccName.setOnLongClickListener(this::onItemLongClick);
+ b.accountRowAccAmounts.setOnLongClickListener(this::onItemLongClick);
+ b.accountExpanderContainer.setOnLongClickListener(this::onItemLongClick);
+ b.accountExpander.setOnLongClickListener(this::onItemLongClick);
+
+ b.accountRowAccName.setOnClickListener(v -> toggleAccountExpanded());
+ b.accountExpanderContainer.setOnClickListener(v -> toggleAccountExpanded());
+ b.accountExpander.setOnClickListener(v -> toggleAccountExpanded());
+ b.accountRowAccAmounts.setOnClickListener(v -> toggleAmountsExpanded());
}
private void toggleAccountExpanded() {
- if (!mAccount.hasSubAccounts())
+ LedgerAccount account = getAccount();
+ if (!account.hasSubAccounts())
return;
debug("accounts", "Account expander clicked");
- // make sure we use the same object as the one in the allAccounts list
- MobileLedgerProfile profile = mAccount.getProfile();
- if (profile == null) {
- return;
- }
- try (Locker ignored = model.lockAccountsForWriting()) {
- LedgerAccount realAccount = model.locateAccount(mAccount.getName());
- if (realAccount == null)
- return;
-
- mAccount = realAccount;
- mAccount.toggleExpanded();
- }
- expanderContainer.animate()
- .rotation(mAccount.isExpanded() ? 0 : 180);
- model.updateDisplayedAccounts();
-
- DbOpQueue.add("update accounts set expanded=? where name=? and profile=?",
- new Object[]{mAccount.isExpanded(), mAccount.getName(), profile.getUuid()
- });
-
+ BaseDAO.runAsync(() -> {
+ Account dbo = account.toDBO();
+ dbo.setExpanded(!dbo.isExpanded());
+ Logger.debug("accounts",
+ String.format(Locale.ROOT, "%s (%d) → %s", account.getName(), dbo.getId(),
+ dbo.isExpanded() ? "expanded" : "collapsed"));
+ DB.get()
+ .getAccountDAO()
+ .updateSync(dbo);
+ });
+ }
+ @NotNull
+ private LedgerAccount getAccount() {
+ return listDiffer.getCurrentList()
+ .get(getBindingAdapterPosition())
+ .toAccount()
+ .getAccount();
}
private void toggleAmountsExpanded() {
- if (mAccount.getAmountCount() <= AMOUNT_LIMIT)
+ LedgerAccount account = getAccount();
+ if (account.getAmountCount() <= AMOUNT_LIMIT)
return;
- mAccount.toggleAmountsExpanded();
- if (mAccount.amountsExpanded()) {
- tvAccountAmounts.setText(mAccount.getAmountsString());
- amountExpanderContainer.setVisibility(View.GONE);
+ account.toggleAmountsExpanded();
+ if (account.amountsExpanded()) {
+ b.accountRowAccAmounts.setText(account.getAmountsString());
+ b.accountRowAmountsExpanderContainer.setVisibility(View.GONE);
}
else {
- tvAccountAmounts.setText(mAccount.getAmountsString(AMOUNT_LIMIT));
- amountExpanderContainer.setVisibility(View.VISIBLE);
+ b.accountRowAccAmounts.setText(account.getAmountsString(AMOUNT_LIMIT));
+ b.accountRowAmountsExpanderContainer.setVisibility(View.VISIBLE);
}
- MobileLedgerProfile profile = mAccount.getProfile();
- if (profile == null)
- return;
-
- DbOpQueue.add("update accounts set amounts_expanded=? where name=? and profile=?",
- new Object[]{mAccount.amountsExpanded(), mAccount.getName(), profile.getUuid()
- });
-
+ BaseDAO.runAsync(() -> {
+ Account dbo = account.toDBO();
+ DB.get()
+ .getAccountDAO()
+ .updateSync(dbo);
+ });
}
private boolean onItemLongClick(View v) {
MainActivity activity = (MainActivity) v.getContext();
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
- final String accountName = mAccount.getName();
+ final String accountName = getAccount().getName();
builder.setTitle(accountName);
builder.setItems(R.array.acc_ctx_menu, (dialog, which) -> {
if (which == 0) {// show transactions
builder.show();
return true;
}
- public void bindToAccount(AccountListItem item) {
- final AccountListItem.Type newType = item.getType();
- setType(newType);
-
- switch (newType) {
- case ACCOUNT:
- LedgerAccount acc = item.getAccount();
-
- debug("accounts", String.format(Locale.US, "Binding to '%s'", acc.getName()));
- Context ctx = row.getContext();
- Resources rm = ctx.getResources();
- mAccount = acc;
-
- row.setTag(acc);
+ @Override
+ public void bind(AccountListItem item, @Nullable List<Object> payloads) {
+ LedgerAccount acc = item.toAccount()
+ .getAccount();
+
+ Change changes = new Change();
+ if (payloads != null) {
+ for (Object p : payloads) {
+ if (p instanceof Change)
+ changes.add((Change) p);
+ }
+ }
+// debug("accounts",
+// String.format(Locale.US, "Binding '%s' to %s", acc.getName(), this));
- tvAccountName.setText(acc.getShortName());
+ Resources rm = b.getRoot()
+ .getContext()
+ .getResources();
- ConstraintLayout.LayoutParams lp =
- (ConstraintLayout.LayoutParams) tvAccountName.getLayoutParams();
- lp.setMarginStart(
- acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) /
- 3);
+ if (changes.has(Change.NAME))
+ b.accountRowAccName.setText(acc.getShortName());
- if (acc.hasSubAccounts()) {
- expanderContainer.setVisibility(View.VISIBLE);
- expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
- }
- else {
- expanderContainer.setVisibility(View.GONE);
- }
+ if (changes.has(Change.LEVEL)) {
+ ConstraintLayout.LayoutParams lp =
+ (ConstraintLayout.LayoutParams) b.flowWrapper.getLayoutParams();
+ lp.setMarginStart(
+ acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 3);
+ }
- int amounts = acc.getAmountCount();
- if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
- tvAccountAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
- amountExpanderContainer.setVisibility(View.VISIBLE);
- }
- else {
- tvAccountAmounts.setText(acc.getAmountsString());
- amountExpanderContainer.setVisibility(View.GONE);
+ if (acc.hasSubAccounts()) {
+ b.accountExpanderContainer.setVisibility(View.VISIBLE);
+
+ if (changes.has(Change.EXPANDED)) {
+ int wantedRotation = acc.isExpanded() ? 0 : 180;
+ if (b.accountExpanderContainer.getRotation() != wantedRotation) {
+// Logger.debug("acc-ui",
+// String.format(Locale.ROOT, "Rotating %s to %d", acc.getName(),
+// wantedRotation));
+ b.accountExpanderContainer.animate()
+ .rotation(wantedRotation);
}
-
- break;
- case HEADER:
- setLastUpdateText(Data.lastAccountsUpdateText.get());
- break;
- default:
- throw new IllegalStateException("Unexpected value: " + newType);
+ }
}
-
- }
- void setLastUpdateText(String text) {
- tvLastUpdate.setText(text);
- }
- private void initLastUpdateObserver() {
- if (lastUpdateObserver != null)
- return;
-
- lastUpdateObserver = (o, arg) -> setLastUpdateText(Data.lastAccountsUpdateText.get());
-
- Data.lastAccountsUpdateText.addObserver(lastUpdateObserver);
- }
- private void dropLastUpdateObserver() {
- if (lastUpdateObserver == null)
- return;
-
- Data.lastAccountsUpdateText.deleteObserver(lastUpdateObserver);
- lastUpdateObserver = null;
- }
- private void setType(AccountListItem.Type newType) {
- if (newType == lastType)
- return;
-
- switch (newType) {
- case ACCOUNT:
- row.setLongClickable(true);
- amountExpanderContainer.setVisibility(View.VISIBLE);
- vAccountNameLayout.setVisibility(View.VISIBLE);
- tvAccountAmounts.setVisibility(View.VISIBLE);
- lLastUpdate.setVisibility(View.GONE);
- dropLastUpdateObserver();
- break;
- case HEADER:
- row.setLongClickable(false);
- tvAccountAmounts.setVisibility(View.GONE);
- amountExpanderContainer.setVisibility(View.GONE);
- vAccountNameLayout.setVisibility(View.GONE);
- lLastUpdate.setVisibility(View.VISIBLE);
- initLastUpdateObserver();
- break;
- default:
- throw new IllegalStateException("Unexpected value: " + newType);
+ else {
+ b.accountExpanderContainer.setVisibility(View.GONE);
}
- lastType = newType;
+ if (changes.has(Change.EXPANDED_AMOUNTS)) {
+ int amounts = acc.getAmountCount();
+ if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
+ b.accountRowAccAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
+ b.accountRowAmountsExpanderContainer.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.accountRowAccAmounts.setText(acc.getAmountsString());
+ b.accountRowAmountsExpanderContainer.setVisibility(View.GONE);
+ }
+ }
}
}
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.account_summary;
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.GeneralBackgroundTasks;
+import net.ktnx.mobileledger.databinding.AccountSummaryFragmentBinding;
+import net.ktnx.mobileledger.db.AccountWithAmounts;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.AccountListItem;
import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerAccount;
+import net.ktnx.mobileledger.ui.FabManager;
import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.Logger;
import org.jetbrains.annotations.NotNull;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
public class AccountSummaryFragment extends MobileLedgerListFragment {
public AccountSummaryAdapter modelAdapter;
+ private AccountSummaryFragmentBinding b;
+ private MenuItem menuShowZeroBalances;
+ private MainModel model;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
debug("flow", "AccountSummaryFragment.onCreateView()");
- return inflater.inflate(R.layout.account_summary_fragment, container, false);
+ b = AccountSummaryFragmentBinding.inflate(inflater, container, false);
+ return b.getRoot();
}
-
@Override
-
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ public SwipeRefreshLayout getRefreshLayout() {
+ return b.accountSwipeRefreshLayout;
+ }
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
debug("flow", "AccountSummaryFragment.onActivityCreated()");
- super.onActivityCreated(savedInstanceState);
+ super.onViewCreated(view, savedInstanceState);
- MainModel model = new ViewModelProvider(requireActivity()).get(MainModel.class);
+ model = new ViewModelProvider(requireActivity()).get(MainModel.class);
Data.backgroundTasksRunning.observe(this.getViewLifecycleOwner(),
this::onBackgroundTaskRunningChanged);
- modelAdapter = new AccountSummaryAdapter(model);
+ modelAdapter = new AccountSummaryAdapter();
MainActivity mainActivity = getMainActivity();
- root = mainActivity.findViewById(R.id.account_root);
LinearLayoutManager llm = new LinearLayoutManager(mainActivity);
llm.setOrientation(RecyclerView.VERTICAL);
- root.setLayoutManager(llm);
- root.setAdapter(modelAdapter);
+ b.accountRoot.setLayoutManager(llm);
+ b.accountRoot.setAdapter(modelAdapter);
DividerItemDecoration did =
new DividerItemDecoration(mainActivity, DividerItemDecoration.VERTICAL);
- root.addItemDecoration(did);
+ b.accountRoot.addItemDecoration(did);
mainActivity.fabShouldShow();
- manageFabOnScroll();
+ if (mainActivity instanceof FabManager.FabHandler)
+ FabManager.handle(mainActivity, b.accountRoot);
- refreshLayout = mainActivity.findViewById(R.id.account_swipe_refresh_layout);
Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
- refreshLayout.setOnRefreshListener(() -> {
+ b.accountSwipeRefreshLayout.setOnRefreshListener(() -> {
debug("ui", "refreshing accounts via swipe");
model.scheduleTransactionListRetrieval();
});
- model.getDisplayedAccounts()
- .observe(getViewLifecycleOwner(), this::onAccountsChanged);
+ Data.observeProfile(this, profile -> onProfileChanged(profile, Boolean.TRUE.equals(
+ model.getShowZeroBalanceAccounts()
+ .getValue())));
+ }
+ @Override
+ public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
+ inflater.inflate(R.menu.account_list, menu);
+
+ menuShowZeroBalances = menu.findItem(R.id.menu_account_list_show_zero_balances);
+ if ((menuShowZeroBalances == null))
+ throw new AssertionError();
+
+ menuShowZeroBalances.setOnMenuItemClickListener(menuItem -> {
+ model.getShowZeroBalanceAccounts()
+ .setValue(Boolean.FALSE.equals(model.getShowZeroBalanceAccounts()
+ .getValue()));
+ return true;
+ });
+
+ model.getShowZeroBalanceAccounts()
+ .observe(this, v -> {
+ menuShowZeroBalances.setChecked(v);
+ onProfileChanged(Data.getProfile(), v);
+ });
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+ private void onProfileChanged(Profile profile, boolean showZeroBalanceAccounts) {
+ if (profile == null)
+ return;
+
+ DB.get()
+ .getAccountDAO()
+ .getAllWithAmounts(profile.getId(), showZeroBalanceAccounts)
+ .observe(getViewLifecycleOwner(), list -> GeneralBackgroundTasks.run(() -> {
+ List<AccountListItem> adapterList = new ArrayList<>();
+ adapterList.add(new AccountListItem.Header(Data.lastAccountsUpdateText));
+ HashMap<String, LedgerAccount> accMap = new HashMap<>();
+ for (AccountWithAmounts dbAcc : list) {
+ LedgerAccount parent = null;
+ String parentName = dbAcc.account.getParentName();
+ if (parentName != null)
+ parent = accMap.get(parentName);
+ if (parent != null)
+ parent.setHasSubAccounts(true);
+ final LedgerAccount account = LedgerAccount.fromDBO(dbAcc, parent);
+ if (account.isVisible())
+ adapterList.add(new AccountListItem.Account(account));
+ accMap.put(dbAcc.account.getName(), account);
+ }
+
+ if (!showZeroBalanceAccounts) {
+ removeZeroAccounts(adapterList);
+ }
+ modelAdapter.setAccounts(adapterList);
+ Data.lastUpdateAccountCount.postValue(adapterList.size() - 1);
+ }));
}
- private void onAccountsChanged(List<AccountListItem> accounts) {
- Logger.debug("async-acc",
- String.format(Locale.US, "fragment: got new account list (%d items)",
- accounts.size()));
- modelAdapter.setAccounts(accounts);
+ private void removeZeroAccounts(List<AccountListItem> list) {
+ boolean removed = true;
+
+ while (removed) {
+ AccountListItem last = null;
+ removed = false;
+ List<AccountListItem> newList = new ArrayList<>();
+
+ for (AccountListItem item : list) {
+ if (last == null) {
+ last = item;
+ continue;
+ }
+
+ if (!last.isAccount() || !last.toAccount()
+ .allAmountsAreZero() || last.toAccount()
+ .getAccount()
+ .isParentOf(
+ item.toAccount()
+ .getAccount()))
+ {
+ newList.add(last);
+ }
+ else {
+ removed = true;
+ }
+
+ last = item;
+ }
+
+ if (last != null) {
+ if (!last.isAccount() || !last.toAccount()
+ .allAmountsAreZero())
+ {
+ newList.add(last);
+ }
+ else {
+ removed = true;
+ }
+ }
+
+ list.clear();
+ list.addAll(newList);
+ }
}
}
+++ /dev/null
-/*
- * Copyright © 2019 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.AsyncTask;
-
-class AsyncCrasher extends AsyncTask<Void, Void, Void> {
- @Override
- protected Void doInBackground(Void... voids) {
- throw new RuntimeException("Simulated crash");
- }
-}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.activity;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.ShortcutInfo;
import android.util.Log;
import android.view.View;
import android.view.animation.AnimationUtils;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
-import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.app.AlertDialog;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentManager;
-import androidx.fragment.app.FragmentPagerAdapter;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
-import androidx.viewpager.widget.ViewPager;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.snackbar.Snackbar;
+import net.ktnx.mobileledger.BackupsActivity;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.async.TransactionAccumulator;
+import net.ktnx.mobileledger.databinding.ActivityMainBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Option;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.ui.FabManager;
import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.account_summary.AccountSummaryFragment;
-import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment;
+import net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity;
+import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity;
import net.ktnx.mobileledger.ui.profiles.ProfilesRecyclerViewAdapter;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Misc;
import org.jetbrains.annotations.NotNull;
* TODO: reports
* */
-public class MainActivity extends ProfileThemedActivity {
+public class MainActivity extends ProfileThemedActivity implements FabManager.FabHandler {
+ public static final String TAG = "main-act";
public static final String STATE_CURRENT_PAGE = "current_page";
public static final String BUNDLE_SAVED_STATE = "bundle_savedState";
public static final String STATE_ACC_FILTER = "account_filter";
- DrawerLayout drawer;
- private View profileListHeadMore, profileListHeadCancel, profileListHeadAddProfile;
- private View bTransactionListCancelDownload;
+ private static final boolean FAB_HIDDEN = false;
+ private static final boolean FAB_SHOWN = true;
+ private ConverterThread converterThread = null;
private SectionsPagerAdapter mSectionsPagerAdapter;
- private ViewPager mViewPager;
- private FloatingActionButton fab;
private ProfilesRecyclerViewAdapter mProfileListAdapter;
private int mCurrentPage;
private boolean mBackMeansToAccountList = false;
- private Toolbar mToolbar;
private DrawerLayout.SimpleDrawerListener drawerListener;
private ActionBarDrawerToggle barDrawerToggle;
- private ViewPager.SimpleOnPageChangeListener pageChangeListener;
- private MobileLedgerProfile profile;
+ private ViewPager2.OnPageChangeCallback pageChangeCallback;
+ private Profile profile;
private MainModel mainModel;
+ private ActivityMainBinding b;
+ private int fabVerticalOffset;
+ private FabManager fabManager;
@Override
protected void onStart() {
super.onStart();
- Logger.debug("MainActivity", "onStart()");
+ Logger.debug(TAG, "onStart()");
- mViewPager.setCurrentItem(mCurrentPage, false);
+ b.mainPager.setCurrentItem(mCurrentPage, false);
}
@Override
protected void onSaveInstanceState(@NotNull Bundle outState) {
super.onSaveInstanceState(outState);
- outState.putInt(STATE_CURRENT_PAGE, mViewPager.getCurrentItem());
+ outState.putInt(STATE_CURRENT_PAGE, b.mainPager.getCurrentItem());
if (mainModel.getAccountFilter()
.getValue() != null)
outState.putString(STATE_ACC_FILTER, mainModel.getAccountFilter()
@Override
protected void onDestroy() {
mSectionsPagerAdapter = null;
- RecyclerView root = findViewById(R.id.nav_profile_list);
- if (root != null)
- root.setAdapter(null);
- if (drawer != null)
- drawer.removeDrawerListener(drawerListener);
+ b.navProfileList.setAdapter(null);
+ b.drawerLayout.removeDrawerListener(drawerListener);
drawerListener = null;
- if (drawer != null)
- drawer.removeDrawerListener(barDrawerToggle);
+ b.drawerLayout.removeDrawerListener(barDrawerToggle);
barDrawerToggle = null;
- if (mViewPager != null)
- mViewPager.removeOnPageChangeListener(pageChangeListener);
- pageChangeListener = null;
+ b.mainPager.unregisterOnPageChangeCallback(pageChangeCallback);
+ pageChangeCallback = null;
super.onDestroy();
}
@Override
- protected void setupProfileColors() {
- final int profileColor = Data.retrieveCurrentThemeIdFromDb();
- Colors.setupTheme(this, profileColor);
- Colors.profileThemeId = profileColor;
- }
- @Override
protected void onResume() {
super.onResume();
fabShouldShow();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
- Logger.debug("MainActivity", "onCreate()/entry");
+ Logger.debug(TAG, "onCreate()/entry");
super.onCreate(savedInstanceState);
- Logger.debug("MainActivity", "onCreate()/after super");
- setContentView(R.layout.activity_main);
+ Logger.debug(TAG, "onCreate()/after super");
+ b = ActivityMainBinding.inflate(getLayoutInflater());
+ setContentView(b.getRoot());
mainModel = new ViewModelProvider(this).get(MainModel.class);
- fab = findViewById(R.id.btn_add_transaction);
- profileListHeadMore = findViewById(R.id.nav_profiles_start_edit);
- profileListHeadCancel = findViewById(R.id.nav_profiles_cancel_edit);
- LinearLayout profileListHeadMoreAndCancel =
- findViewById(R.id.nav_profile_list_head_buttons);
- profileListHeadAddProfile = findViewById(R.id.nav_new_profile_button);
- drawer = findViewById(R.id.drawer_layout);
- bTransactionListCancelDownload = findViewById(R.id.transaction_list_cancel_download);
- mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
- mViewPager = findViewById(R.id.root_frame);
+ mSectionsPagerAdapter = new SectionsPagerAdapter(this);
Bundle extra = getIntent().getBundleExtra(BUNDLE_SAVED_STATE);
if (extra != null && savedInstanceState == null)
savedInstanceState = extra;
- mToolbar = findViewById(R.id.toolbar);
- setSupportActionBar(mToolbar);
+ setSupportActionBar(b.toolbar);
Data.observeProfile(this, this::onProfileChanged);
Data.profiles.observe(this, this::onProfileListChanged);
+
Data.backgroundTaskProgress.observe(this, this::onRetrieveProgress);
Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged);
if (barDrawerToggle == null) {
- barDrawerToggle = new ActionBarDrawerToggle(this, drawer, mToolbar,
- R.string.navigation_drawer_open, R.string.navigation_drawer_close);
- drawer.addDrawerListener(barDrawerToggle);
+ barDrawerToggle = new ActionBarDrawerToggle(this, b.drawerLayout, b.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
+ b.drawerLayout.addDrawerListener(barDrawerToggle);
}
barDrawerToggle.syncState();
try {
PackageInfo pi = getApplicationContext().getPackageManager()
.getPackageInfo(getPackageName(), 0);
- ((TextView) findViewById(R.id.nav_upper).findViewById(
- R.id.drawer_version_text)).setText(pi.versionName);
- ((TextView) findViewById(R.id.no_profiles_layout).findViewById(
- R.id.drawer_version_text)).setText(pi.versionName);
+ ((TextView) b.navUpper.findViewById(R.id.drawer_version_text)).setText(pi.versionName);
+ ((TextView) b.noProfilesLayout.findViewById(R.id.drawer_version_text)).setText(pi.versionName);
}
catch (Exception e) {
e.printStackTrace();
markDrawerItemCurrent(R.id.nav_account_summary);
- mViewPager.setAdapter(mSectionsPagerAdapter);
+ b.mainPager.setAdapter(mSectionsPagerAdapter);
+ b.mainPager.setOffscreenPageLimit(1);
- if (pageChangeListener == null) {
- pageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
+ if (pageChangeCallback == null) {
+ pageChangeCallback = new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
mCurrentPage = position;
markDrawerItemCurrent(R.id.nav_latest_transactions);
break;
default:
- Log.e("MainActivity",
- String.format("Unexpected page index %d", position));
+ Log.e(TAG, String.format("Unexpected page index %d", position));
}
super.onPageSelected(position);
}
};
- mViewPager.addOnPageChangeListener(pageChangeListener);
+ b.mainPager.registerOnPageChangeCallback(pageChangeCallback);
}
mCurrentPage = 0;
.setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
}
- findViewById(R.id.btn_no_profiles_add).setOnClickListener(
- v -> startEditProfileActivity(null));
+ b.btnNoProfilesAdd.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
+ b.btnRestore.setOnClickListener(v -> BackupsActivity.start(this));
- findViewById(R.id.btn_add_transaction).setOnClickListener(this::fabNewTransactionClicked);
+ b.btnAddTransaction.setOnClickListener(this::fabNewTransactionClicked);
- findViewById(R.id.nav_new_profile_button).setOnClickListener(
- v -> startEditProfileActivity(null));
+ b.navNewProfileButton.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
- RecyclerView root = findViewById(R.id.nav_profile_list);
- if (root == null)
- throw new RuntimeException("Can't get hold on the transaction value view");
+ b.transactionListCancelDownload.setOnClickListener(this::onStopTransactionRefreshClick);
if (mProfileListAdapter == null)
mProfileListAdapter = new ProfilesRecyclerViewAdapter();
- root.setAdapter(mProfileListAdapter);
+ b.navProfileList.setAdapter(mProfileListAdapter);
mProfileListAdapter.editingProfiles.observe(this, newValue -> {
if (newValue) {
- profileListHeadMore.setVisibility(View.GONE);
- profileListHeadCancel.setVisibility(View.VISIBLE);
- profileListHeadAddProfile.setVisibility(View.VISIBLE);
- if (drawer.isDrawerOpen(GravityCompat.START)) {
- profileListHeadMore.startAnimation(
+ b.navProfilesStartEdit.setVisibility(View.GONE);
+ b.navProfilesCancelEdit.setVisibility(View.VISIBLE);
+ b.navNewProfileButton.setVisibility(View.VISIBLE);
+ if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ b.navProfilesStartEdit.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
- profileListHeadCancel.startAnimation(
+ b.navProfilesCancelEdit.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
- profileListHeadAddProfile.startAnimation(
+ b.navNewProfileButton.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
}
}
else {
- profileListHeadCancel.setVisibility(View.GONE);
- profileListHeadMore.setVisibility(View.VISIBLE);
- profileListHeadAddProfile.setVisibility(View.GONE);
- if (drawer.isDrawerOpen(GravityCompat.START)) {
- profileListHeadCancel.startAnimation(
+ b.navProfilesCancelEdit.setVisibility(View.GONE);
+ b.navProfilesStartEdit.setVisibility(View.VISIBLE);
+ b.navNewProfileButton.setVisibility(View.GONE);
+ if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ b.navProfilesCancelEdit.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
- profileListHeadMore.startAnimation(
+ b.navProfilesStartEdit.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
- profileListHeadAddProfile.startAnimation(
+ b.navNewProfileButton.startAnimation(
AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
}
}
mProfileListAdapter.notifyDataSetChanged();
});
+ fabManager = new FabManager(b.btnAddTransaction);
+
LinearLayoutManager llm = new LinearLayoutManager(this);
llm.setOrientation(RecyclerView.VERTICAL);
- root.setLayoutManager(llm);
+ b.navProfileList.setLayoutManager(llm);
- profileListHeadMore.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
- profileListHeadCancel.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
- profileListHeadMoreAndCancel.setOnClickListener(
- (v) -> mProfileListAdapter.flipEditingProfiles());
+ b.navProfilesStartEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
+ b.navProfilesCancelEdit.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
+ b.navProfileListHeadButtons.setOnClickListener((v) -> mProfileListAdapter.flipEditingProfiles());
if (drawerListener == null) {
drawerListener = new DrawerLayout.SimpleDrawerListener() {
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
if (slideOffset > 0.2)
- fabHide();
+ fabManager.hideFab();
}
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerOpened(drawerView);
mProfileListAdapter.setAnimationsEnabled(true);
Data.drawerOpen.setValue(true);
- fabHide();
+ fabManager.hideFab();
}
};
- drawer.addDrawerListener(drawerListener);
+ b.drawerLayout.addDrawerListener(drawerListener);
}
Data.drawerOpen.observe(this, open -> {
if (open)
- drawer.open();
+ b.drawerLayout.open();
else
- drawer.close();
+ b.drawerLayout.close();
});
mainModel.getUpdateError()
if (error == null)
return;
- Snackbar.make(mViewPager, error, Snackbar.LENGTH_LONG)
+ Snackbar.make(b.mainPager, error, Snackbar.LENGTH_INDEFINITE)
.show();
mainModel.clearUpdateError();
});
Data.lastUpdateDate.observe(this, date -> refreshLastUpdateInfo());
Data.lastUpdateTransactionCount.observe(this, date -> refreshLastUpdateInfo());
Data.lastUpdateAccountCount.observe(this, date -> refreshLastUpdateInfo());
+ b.navAccountSummary.setOnClickListener(this::onAccountSummaryClicked);
+ b.navLatestTransactions.setOnClickListener(this::onLatestTransactionsClicked);
+ b.navPatterns.setOnClickListener(this::onPatternsClick);
+ b.navBackupRestore.setOnClickListener(this::onBackupRestoreClick);
+ }
+ private void onBackupRestoreClick(View view) {
+ Intent intent = new Intent(this, BackupsActivity.class);
+ startActivity(intent);
+ }
+ private void onPatternsClick(View view) {
+ Intent intent = new Intent(this, TemplatesActivity.class);
+ startActivity(intent);
}
private void scheduleDataRetrievalIfStale(long lastUpdate) {
long now = new Date().getTime();
mainModel.scheduleTransactionListRetrieval();
}
}
- private void createShortcuts(List<MobileLedgerProfile> list) {
+ private void createShortcuts(@NotNull List<Profile> list) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1)
return;
ShortcutManager sm = getSystemService(ShortcutManager.class);
List<ShortcutInfo> shortcuts = new ArrayList<>();
int i = 0;
- for (MobileLedgerProfile p : list) {
+ for (Profile p : list) {
if (shortcuts.size() >= sm.getMaxShortcutCountPerActivity())
break;
- if (!p.isPostingPermitted())
+ if (!p.permitPosting())
continue;
final ShortcutInfo.Builder builder =
- new ShortcutInfo.Builder(this, "new_transaction_" + p.getUuid());
+ new ShortcutInfo.Builder(this, "new_transaction_" + p.getId());
ShortcutInfo si = builder.setShortLabel(p.getName())
.setIcon(Icon.createWithResource(this,
R.drawable.thick_plus_icon))
.setIntent(new Intent(Intent.ACTION_VIEW, null, this,
- NewTransactionActivity.class).putExtra("profile_uuid",
- p.getUuid()))
+ NewTransactionActivity.class).putExtra(
+ ProfileThemedActivity.PARAM_PROFILE_ID, p.getId())
+ .putExtra(
+ ProfileThemedActivity.PARAM_THEME,
+ p.getTheme()))
.setRank(i)
.build();
shortcuts.add(si);
}
sm.setDynamicShortcuts(shortcuts);
}
- private void onProfileListChanged(List<MobileLedgerProfile> newList) {
- if ((newList == null) || newList.isEmpty()) {
- findViewById(R.id.no_profiles_layout).setVisibility(View.VISIBLE);
- findViewById(R.id.main_app_layout).setVisibility(View.GONE);
+ private void onProfileListChanged(@NotNull List<Profile> newList) {
+ createShortcuts(newList);
+
+ if (newList.isEmpty()) {
+ b.noProfilesLayout.setVisibility(View.VISIBLE);
+ b.mainAppLayout.setVisibility(View.GONE);
return;
}
- findViewById(R.id.main_app_layout).setVisibility(View.VISIBLE);
- findViewById(R.id.no_profiles_layout).setVisibility(View.GONE);
+ b.mainAppLayout.setVisibility(View.VISIBLE);
+ b.noProfilesLayout.setVisibility(View.GONE);
- findViewById(R.id.nav_profile_list).setMinimumHeight(
+ b.navProfileList.setMinimumHeight(
(int) (getResources().getDimension(R.dimen.thumb_row_height) * newList.size()));
Logger.debug("profiles", "profile list changed");
- mProfileListAdapter.notifyDataSetChanged();
+ mProfileListAdapter.setProfileList(newList);
+
+ final Profile currentProfile = Data.getProfile();
+ Profile replacementProfile = null;
+ if (currentProfile != null) {
+ for (Profile p : newList) {
+ if (p.getId() == currentProfile.getId()) {
+ replacementProfile = p;
+ break;
+ }
+ }
+ }
- createShortcuts(newList);
+ if (replacementProfile == null) {
+ Logger.debug(TAG, "Switching profile because the current is no longer available");
+ Data.setCurrentProfile(newList.get(0));
+ }
+ else {
+ Data.setCurrentProfile(replacementProfile);
+ }
}
/**
* called when the current profile has changed
*/
- private void onProfileChanged(MobileLedgerProfile profile) {
- if (this.profile == null) {
- if (profile == null)
- return;
- }
- else {
- if (this.profile.equals(profile))
+ private void onProfileChanged(@Nullable Profile newProfile) {
+ if (this.profile != null) {
+ if (this.profile.equals(newProfile))
return;
}
- boolean haveProfile = profile != null;
+ boolean haveProfile = newProfile != null;
if (haveProfile)
- setTitle(profile.getName());
+ setTitle(newProfile.getName());
else
setTitle(R.string.app_name);
- mainModel.setProfile(profile);
-
- this.profile = profile;
-
- int newProfileTheme = haveProfile ? profile.getThemeHue() : -1;
+ int newProfileTheme = haveProfile ? newProfile.getTheme() : Colors.DEFAULT_HUE_DEG;
if (newProfileTheme != Colors.profileThemeId) {
Logger.debug("profiles",
String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId,
return;
}
- findViewById(R.id.no_profiles_layout).setVisibility(haveProfile ? View.GONE : View.VISIBLE);
- findViewById(R.id.pager_layout).setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
+ final boolean sameProfileId = (newProfile != null) && (this.profile != null) &&
+ this.profile.getId() == newProfile.getId();
- mProfileListAdapter.notifyDataSetChanged();
+ this.profile = newProfile;
- mainModel.clearAccounts();
- mainModel.clearTransactions();
+ b.noProfilesLayout.setVisibility(haveProfile ? View.GONE : View.VISIBLE);
+ b.pagerLayout.setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
- if (haveProfile) {
- mainModel.scheduleAccountListReload();
- Logger.debug("transactions", "requesting list reload");
- mainModel.scheduleTransactionListReload();
+ mProfileListAdapter.notifyDataSetChanged();
- if (profile.isPostingPermitted()) {
- mToolbar.setSubtitle(null);
- fab.show();
+ if (haveProfile) {
+ if (newProfile.permitPosting()) {
+ b.toolbar.setSubtitle(null);
+ b.btnAddTransaction.show();
}
else {
- mToolbar.setSubtitle(R.string.profile_subtitle_read_only);
- fab.hide();
+ b.toolbar.setSubtitle(R.string.profile_subtitle_read_only);
+ b.btnAddTransaction.hide();
}
}
else {
- mToolbar.setSubtitle(null);
- fab.hide();
+ b.toolbar.setSubtitle(null);
+ b.btnAddTransaction.hide();
}
updateLastUpdateTextFromDB();
+
+ if (sameProfileId) {
+ Logger.debug(TAG, String.format(Locale.ROOT, "Short-cut profile 'changed' to %d",
+ newProfile.getId()));
+ return;
+ }
+
+ mainModel.getAccountFilter()
+ .observe(this, this::onAccountFilterChanged);
+
+ mainModel.stopTransactionsRetrieval();
+ mainModel.clearTransactions();
+ }
+ private void onAccountFilterChanged(String accFilter) {
+ Logger.debug(TAG, "account filter changed, reloading transactions");
+// mainModel.scheduleTransactionListReload();
+ LiveData<List<TransactionWithAccounts>> transactions =
+ new MutableLiveData<>(new ArrayList<>());
+ if (profile != null) {
+ if (accFilter == null || accFilter.isEmpty()) {
+ transactions = DB.get()
+ .getTransactionDAO()
+ .getAllWithAccounts(profile.getId());
+ }
+ else {
+ transactions = DB.get()
+ .getTransactionDAO()
+ .getAllWithAccountsFiltered(profile.getId(), accFilter);
+ }
+ }
+
+ transactions.observe(this, list -> {
+ Logger.debug(TAG,
+ String.format(Locale.ROOT, "got transaction list from DB (%d transactions)",
+ list.size()));
+
+ if (converterThread != null)
+ converterThread.interrupt();
+ converterThread = new ConverterThread(mainModel, list, accFilter);
+ converterThread.start();
+ });
}
private void profileThemeChanged() {
// un-hook all observed LiveData
Data.lastUpdateAccountCount.removeObservers(this);
Data.lastUpdateDate.removeObservers(this);
+ Logger.debug(TAG, "profileThemeChanged(): recreating activity");
recreate();
}
- public void startEditProfileActivity(MobileLedgerProfile profile) {
- Intent intent = new Intent(this, ProfileDetailActivity.class);
- Bundle args = new Bundle();
- if (profile != null) {
- int index = Data.getProfileIndex(profile);
- if (index != -1)
- intent.putExtra(ProfileDetailFragment.ARG_ITEM_ID, index);
- }
- intent.putExtras(args);
- startActivity(intent, args);
- }
public void fabNewTransactionClicked(View view) {
Intent intent = new Intent(this, NewTransactionActivity.class);
+ intent.putExtra(ProfileThemedActivity.PARAM_PROFILE_ID, profile.getId());
+ intent.putExtra(ProfileThemedActivity.PARAM_THEME, profile.getTheme());
startActivity(intent);
overridePendingTransition(R.anim.slide_in_up, R.anim.dummy);
}
public void markDrawerItemCurrent(int id) {
- TextView item = drawer.findViewById(id);
+ TextView item = b.drawerLayout.findViewById(id);
item.setBackgroundColor(Colors.tableRowDarkBG);
- LinearLayout actions = drawer.findViewById(R.id.nav_actions);
- for (int i = 0; i < actions.getChildCount(); i++) {
- View view = actions.getChildAt(i);
+ for (int i = 0; i < b.navActions.getChildCount(); i++) {
+ View view = b.navActions.getChildAt(i);
if (view.getId() != id) {
view.setBackgroundColor(Color.TRANSPARENT);
}
}
}
public void onAccountSummaryClicked(View view) {
- drawer.closeDrawers();
+ b.drawerLayout.closeDrawers();
showAccountSummaryFragment();
}
private void showAccountSummaryFragment() {
- mViewPager.setCurrentItem(0, true);
+ b.mainPager.setCurrentItem(0, true);
mainModel.getAccountFilter()
.setValue(null);
}
public void onLatestTransactionsClicked(View view) {
- drawer.closeDrawers();
+ b.drawerLayout.closeDrawers();
showTransactionsFragment(null);
}
public void showTransactionsFragment(String accName) {
mainModel.getAccountFilter()
.setValue(accName);
- mViewPager.setCurrentItem(1, true);
+ b.mainPager.setCurrentItem(1, true);
}
public void showAccountTransactions(String accountName) {
mBackMeansToAccountList = true;
}
@Override
public void onBackPressed() {
- DrawerLayout drawer = findViewById(R.id.drawer_layout);
- if (drawer.isDrawerOpen(GravityCompat.START)) {
- drawer.closeDrawer(GravityCompat.START);
+ if (b.drawerLayout.isDrawerOpen(GravityCompat.START)) {
+ b.drawerLayout.closeDrawer(GravityCompat.START);
}
else {
- if (mBackMeansToAccountList && (mViewPager.getCurrentItem() == 1)) {
+ if (mBackMeansToAccountList && (b.mainPager.getCurrentItem() == 1)) {
mainModel.getAccountFilter()
.setValue(null);
showAccountSummaryFragment();
mBackMeansToAccountList = false;
}
else {
- Logger.debug("fragments", String.format(Locale.ENGLISH, "manager stack: %d",
+ Logger.debug(TAG, String.format(Locale.ENGLISH, "manager stack: %d",
getSupportFragmentManager().getBackStackEntryCount()));
super.onBackPressed();
if (profile == null)
return;
- long lastUpdate = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
-
- Logger.debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", lastUpdate));
- if (lastUpdate == 0) {
- Data.lastUpdateDate.postValue(null);
- }
- else {
- Data.lastUpdateDate.postValue(new Date(lastUpdate));
- }
-
- scheduleDataRetrievalIfStale(lastUpdate);
-
+ DB.get()
+ .getOptionDAO()
+ .load(profile.getId(), Option.OPT_LAST_SCRAPE)
+ .observe(this, opt -> {
+ long lastUpdate = 0;
+ if (opt != null) {
+ try {
+ lastUpdate = Long.parseLong(opt.getValue());
+ }
+ catch (NumberFormatException ex) {
+ Logger.debug(TAG, String.format("Error parsing '%s' as long", opt.getValue()),
+ ex);
+ }
+ }
+
+ if (lastUpdate == 0) {
+ Data.lastUpdateDate.postValue(null);
+ }
+ else {
+ Data.lastUpdateDate.postValue(new Date(lastUpdate));
+ }
+
+ scheduleDataRetrievalIfStale(lastUpdate);
+ });
}
private void refreshLastUpdateInfo() {
final int formatFlags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR |
Integer transactionCount = Data.lastUpdateTransactionCount.getValue();
Date lastUpdate = Data.lastUpdateDate.getValue();
if (lastUpdate == null) {
- Data.lastTransactionsUpdateText.set("----");
- Data.lastAccountsUpdateText.set("----");
+ Data.lastTransactionsUpdateText.setValue("----");
+ Data.lastAccountsUpdateText.setValue("----");
}
else {
- Data.lastTransactionsUpdateText.set(
+ Data.lastTransactionsUpdateText.setValue(
String.format(Objects.requireNonNull(Data.locale.getValue()),
templateForTransactions,
transactionCount == null ? 0 : transactionCount,
DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
- Data.lastAccountsUpdateText.set(
+ Data.lastAccountsUpdateText.setValue(
String.format(Objects.requireNonNull(Data.locale.getValue()),
templateForAccounts, accountCount == null ? 0 : accountCount,
DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
}
}
public void onStopTransactionRefreshClick(View view) {
- Logger.debug("interactive", "Cancelling transactions refresh");
+ Logger.debug(TAG, "Cancelling transactions refresh");
mainModel.stopTransactionsRetrieval();
- bTransactionListCancelDownload.setEnabled(false);
+ b.transactionListCancelDownload.setEnabled(false);
}
public void onRetrieveRunningChanged(Boolean running) {
- final View progressLayout = findViewById(R.id.transaction_progress_layout);
if (running) {
- ProgressBar progressBar = findViewById(R.id.transaction_list_progress_bar);
- bTransactionListCancelDownload.setEnabled(true);
+ b.transactionListCancelDownload.setEnabled(true);
ColorStateList csl = Colors.getColorStateList();
- progressBar.setIndeterminateTintList(csl);
- progressBar.setProgressTintList(csl);
- progressBar.setIndeterminate(true);
+ b.transactionListProgressBar.setIndeterminateTintList(csl);
+ b.transactionListProgressBar.setProgressTintList(csl);
+ b.transactionListProgressBar.setIndeterminate(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- progressBar.setProgress(0, false);
+ b.transactionListProgressBar.setProgress(0, false);
}
else {
- progressBar.setProgress(0);
+ b.transactionListProgressBar.setProgress(0);
}
- progressLayout.setVisibility(View.VISIBLE);
+ b.transactionProgressLayout.setVisibility(View.VISIBLE);
}
else {
- progressLayout.setVisibility(View.GONE);
+ b.transactionProgressLayout.setVisibility(View.GONE);
}
}
- public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
- ProgressBar progressBar = findViewById(R.id.transaction_list_progress_bar);
-
- if (progress.getState() == RetrieveTransactionsTask.ProgressState.FINISHED) {
- Logger.debug("progress", "Done");
- findViewById(R.id.transaction_progress_layout).setVisibility(View.GONE);
+ public void onRetrieveProgress(@Nullable RetrieveTransactionsTask.Progress progress) {
+ if (progress == null ||
+ progress.getState() == RetrieveTransactionsTask.ProgressState.FINISHED)
+ {
+ Logger.debug(TAG, "progress: Done");
+ b.transactionProgressLayout.setVisibility(View.GONE);
mainModel.transactionRetrievalDone();
- if (progress.getError() != null) {
- Snackbar.make(mViewPager, progress.getError(), Snackbar.LENGTH_LONG)
- .show();
+ String error = (progress == null) ? null : progress.getError();
+ if (error != null) {
+ if (error.equals(RetrieveTransactionsTask.Result.ERR_JSON_PARSER_ERROR))
+ error = getResources().getString(R.string.err_json_parser_error);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(error);
+ builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
+ Logger.debug(TAG, "will start profile editor");
+ ProfileDetailActivity.start(this, profile);
+ });
+ builder.create()
+ .show();
return;
}
}
- bTransactionListCancelDownload.setEnabled(true);
+ b.transactionListCancelDownload.setEnabled(true);
// ColorStateList csl = Colors.getColorStateList();
// progressBar.setIndeterminateTintList(csl);
// progressBar.setProgressTintList(csl);
// progressBar.setProgress(0, false);
// else
// progressBar.setProgress(0);
- findViewById(R.id.transaction_progress_layout).setVisibility(View.VISIBLE);
+ b.transactionProgressLayout.setVisibility(View.VISIBLE);
if (progress.isIndeterminate() || (progress.getTotal() <= 0)) {
- progressBar.setIndeterminate(true);
- Logger.debug("progress", "indeterminate");
+ b.transactionListProgressBar.setIndeterminate(true);
+ Logger.debug(TAG, "progress: indeterminate");
}
else {
- if (progressBar.isIndeterminate()) {
- progressBar.setIndeterminate(false);
+ if (b.transactionListProgressBar.isIndeterminate()) {
+ b.transactionListProgressBar.setIndeterminate(false);
}
-// Logger.debug("progress",
-// String.format(Locale.US, "%d/%d", progress.getProgress(), progress.getTotal
+// Logger.debug(TAG,
+// String.format(Locale.US, "progress: %d/%d", progress.getProgress(),
+// progress.getTotal
// ()));
- progressBar.setMax(progress.getTotal());
+ b.transactionListProgressBar.setMax(progress.getTotal());
// for some reason animation doesn't work - no progress is shown (stick at 0)
// on lineageOS 14.1 (Nougat, 7.1.2)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
- progressBar.setProgress(progress.getProgress(), false);
+ b.transactionListProgressBar.setProgress(progress.getProgress(), false);
else
- progressBar.setProgress(progress.getProgress());
+ b.transactionListProgressBar.setProgress(progress.getProgress());
}
}
public void fabShouldShow() {
- if ((profile != null) && profile.isPostingPermitted() && !drawer.isOpen())
- fab.show();
- else
- fabHide();
+ if ((profile != null) && profile.permitPosting() && !b.drawerLayout.isOpen())
+ fabManager.showFab();
}
- public void fabHide() {
- fab.hide();
+ @Override
+ public Context getContext() {
+ return this;
}
+ @Override
+ public void showManagedFab() {
+ fabShouldShow();
+ }
+ @Override
+ public void hideManagedFab() {
+ fabManager.hideFab();
+ }
+ public static class SectionsPagerAdapter extends FragmentStateAdapter {
- public static class SectionsPagerAdapter extends FragmentPagerAdapter {
-
- SectionsPagerAdapter(FragmentManager fm) {
- super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
+ public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
+ super(fragmentActivity);
}
-
@NotNull
@Override
- public Fragment getItem(int position) {
- Logger.debug("main",
- String.format(Locale.ENGLISH, "Switching to fragment %d", position));
+ public Fragment createFragment(int position) {
+ Logger.debug(TAG, String.format(Locale.ENGLISH, "Switching to fragment %d", position));
switch (position) {
case 0:
-// debug("flow", "Creating account summary fragment");
+// debug(TAG, "Creating account summary fragment");
return new AccountSummaryFragment();
case 1:
return new TransactionListFragment();
}
@Override
- public int getCount() {
+ public int getItemCount() {
return 2;
}
}
+
+ static private class ConverterThread extends Thread {
+ private final List<TransactionWithAccounts> list;
+ private final MainModel model;
+ private final String accFilter;
+ public ConverterThread(@NonNull MainModel model,
+ @NonNull List<TransactionWithAccounts> list, String accFilter) {
+ this.model = model;
+ this.list = list;
+ this.accFilter = accFilter;
+ }
+ @Override
+ public void run() {
+ TransactionAccumulator accumulator = new TransactionAccumulator(accFilter, accFilter);
+
+ for (TransactionWithAccounts tr : list) {
+ if (isInterrupted()) {
+ Logger.debug(TAG, "ConverterThread bailing out on interrupt");
+ return;
+ }
+ accumulator.put(new LedgerTransaction(tr));
+ }
+
+ if (isInterrupted()) {
+ Logger.debug(TAG, "ConverterThread bailing out on interrupt");
+ return;
+ }
+
+ Logger.debug(TAG, "ConverterThread publishing results");
+
+ Misc.onMainThread(() -> accumulator.publishResults(model));
+ }
+ }
}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.Bundle;
-import android.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.navigation.NavController;
-import androidx.navigation.fragment.NavHostFragment;
-
-import net.ktnx.mobileledger.BuildConfig;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.SendTransactionTask;
-import net.ktnx.mobileledger.async.TaskCallback;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-
-import java.util.Objects;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class NewTransactionActivity extends ProfileThemedActivity implements TaskCallback,
- NewTransactionFragment.OnNewTransactionFragmentInteractionListener {
- private NavController navController;
- private NewTransactionModel model;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.activity_new_transaction);
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
- Data.observeProfile(this,
- mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName()));
-
- NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
- getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
- navController = navHostFragment.getNavController();
-
- Objects.requireNonNull(getSupportActionBar())
- .setDisplayHomeAsUpEnabled(true);
-
- model = new ViewModelProvider(this).get(NewTransactionModel.class);
- }
- @Override
- protected void initProfile() {
- String profileUUID = getIntent().getStringExtra("profile_uuid");
-
- if (profileUUID != null) {
- mProfile = Data.getProfile(profileUUID);
- if (mProfile == null)
- finish();
- Data.setCurrentProfile(mProfile);
- }
- else
- super.initProfile();
- }
- @Override
- public void finish() {
- super.finish();
- overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
- }
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- finish();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
- public void onTransactionSave(LedgerTransaction tr) {
- navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
- try {
-
- SendTransactionTask saver =
- new SendTransactionTask(this, mProfile, model.getSimulateSave());
- saver.execute(tr);
- }
- catch (Exception e) {
- debug("new-transaction", "Unknown error", e);
-
- Bundle b = new Bundle();
- b.putString("error", "unknown error");
- navController.navigate(R.id.newTransactionFragment, b);
- }
- }
- public void simulateCrash(MenuItem item) {
- debug("crash", "Will crash intentionally");
- new AsyncCrasher().execute();
- }
- public boolean onCreateOptionsMenu(Menu menu) {
- // Inflate the menu; this adds items to the action bar if it is present.
- getMenuInflater().inflate(R.menu.new_transaction, menu);
-
- if (BuildConfig.DEBUG) {
- menu.findItem(R.id.action_simulate_crash)
- .setVisible(true);
- menu.findItem(R.id.action_simulate_save)
- .setVisible(true);
- }
-
- model.observeSimulateSave(this, state -> {
- menu.findItem(R.id.action_simulate_save)
- .setChecked(state);
- findViewById(R.id.simulationLabel).setVisibility(state ? View.VISIBLE : View.GONE);
- });
-
- return true;
- }
-
-
- public int dp2px(float dp) {
- return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
- getResources().getDisplayMetrics()));
- }
- @Override
- public void done(String error) {
- Bundle b = new Bundle();
- if (error != null) {
- b.putString("error", error);
- navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
- }
- else
- navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
- }
- public void toggleSimulateSave(MenuItem item) {
- model.toggleSimulateSave();
- }
-
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.renderscript.RSInvalidStateException;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ProgressBar;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * A simple {@link Fragment} subclass.
- * Activities that contain this fragment must implement the
- * {@link OnNewTransactionFragmentInteractionListener} interface
- * to handle interaction events.
- */
-
-// TODO: offer to undo account remove-on-swipe
-// TODO: transaction-level comment
-
-public class NewTransactionFragment extends Fragment {
- private NewTransactionItemsAdapter listAdapter;
- private NewTransactionModel viewModel;
- private FloatingActionButton fab;
- private OnNewTransactionFragmentInteractionListener mListener;
- private MobileLedgerProfile mProfile;
- public NewTransactionFragment() {
- // Required empty public constructor
- setHasOptionsMenu(true);
- }
- @Override
- public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- final FragmentActivity activity = getActivity();
-
- inflater.inflate(R.menu.new_transaction_fragment, menu);
- menu.findItem(R.id.action_reset_new_transaction_activity)
- .setOnMenuItemClickListener(item -> {
- listAdapter.reset();
- return true;
- });
-
- final MenuItem toggleCurrencyItem = menu.findItem(R.id.toggle_currency);
- toggleCurrencyItem.setOnMenuItemClickListener(item -> {
- viewModel.toggleCurrencyVisible();
- return true;
- });
- if (activity != null)
- viewModel.showCurrency.observe(activity, toggleCurrencyItem::setChecked);
-
- final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
- toggleCommentsItem.setOnMenuItemClickListener(item -> {
- viewModel.toggleShowComments();
- return true;
- });
- if (activity != null)
- viewModel.showComments.observe(activity, toggleCommentsItem::setChecked);
- }
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- return inflater.inflate(R.layout.fragment_new_transaction, container, false);
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- FragmentActivity activity = getActivity();
- if (activity == null)
- throw new RSInvalidStateException(
- "getActivity() returned null within onActivityCreated()");
-
- viewModel = new ViewModelProvider(activity).get(NewTransactionModel.class);
- viewModel.observeDataProfile(this);
- mProfile = Data.getProfile();
- listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
-
- RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
- list.setAdapter(listAdapter);
- list.setLayoutManager(new LinearLayoutManager(activity));
-
- Data.observeProfile(getViewLifecycleOwner(), profile -> {
- mProfile = profile;
- listAdapter.setProfile(profile);
- });
- listAdapter.notifyDataSetChanged();
- viewModel.isSubmittable()
- .observe(getViewLifecycleOwner(), isSubmittable -> {
- if (isSubmittable) {
- if (fab != null) {
- fab.show();
- }
- }
- else {
- if (fab != null) {
- fab.hide();
- }
- }
- });
-// viewModel.checkTransactionSubmittable(listAdapter);
-
- fab = activity.findViewById(R.id.fab);
- fab.setOnClickListener(v -> onFabPressed());
-
- boolean keep = false;
-
- Bundle args = getArguments();
- if (args != null) {
- String error = args.getString("error");
- if (error != null) {
- Logger.debug("new-trans-f", String.format("Got error: %s", error));
- Snackbar.make(list, error, Snackbar.LENGTH_LONG)
- .show();
- keep = true;
- }
- }
-
- int focused = 0;
- if (savedInstanceState != null) {
- keep |= savedInstanceState.getBoolean("keep", true);
- focused = savedInstanceState.getInt("focused", 0);
- }
-
- if (!keep)
- viewModel.reset();
- else {
- viewModel.setFocusedItem(focused);
- }
-
- ProgressBar p = activity.findViewById(R.id.progressBar);
- viewModel.observeBusyFlag(getViewLifecycleOwner(), isBusy -> {
- if (isBusy) {
-// Handler h = new Handler();
-// h.postDelayed(() -> {
-// if (viewModel.getBusyFlag())
-// p.setVisibility(View.VISIBLE);
-//
-// }, 10);
- p.setVisibility(View.VISIBLE);
- }
- else
- p.setVisibility(View.INVISIBLE);
- });
- }
- @Override
- public void onSaveInstanceState(@NonNull Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putBoolean("keep", true);
- final int focusedItem = viewModel.getFocusedItem();
- outState.putInt("focused", focusedItem);
- }
- private void onFabPressed() {
- fab.hide();
- Misc.hideSoftKeyboard(this);
- if (mListener != null) {
- SimpleDate date = viewModel.getDate();
- LedgerTransaction tr =
- new LedgerTransaction(null, date, viewModel.getDescription(), mProfile);
-
- tr.setComment(viewModel.getComment());
- LedgerTransactionAccount emptyAmountAccount = null;
- float emptyAmountAccountBalance = 0;
- for (int i = 0; i < viewModel.getAccountCount(); i++) {
- LedgerTransactionAccount acc =
- new LedgerTransactionAccount(viewModel.getAccount(i));
- if (acc.getAccountName()
- .trim()
- .isEmpty())
- continue;
-
- if (acc.isAmountSet()) {
- emptyAmountAccountBalance += acc.getAmount();
- }
- else {
- emptyAmountAccount = acc;
- }
-
- tr.addAccount(acc);
- }
-
- if (emptyAmountAccount != null)
- emptyAmountAccount.setAmount(-emptyAmountAccountBalance);
-
- mListener.onTransactionSave(tr);
- }
- }
-
- @Override
- public void onAttach(@NotNull Context context) {
- super.onAttach(context);
- if (context instanceof OnNewTransactionFragmentInteractionListener) {
- mListener = (OnNewTransactionFragmentInteractionListener) context;
- }
- else {
- throw new RuntimeException(
- context.toString() + " must implement OnFragmentInteractionListener");
- }
- }
-
- @Override
- public void onDetach() {
- super.onDetach();
- mListener = null;
- }
-
- /**
- * This interface must be implemented by activities that contain this
- * fragment to allow an interaction in this fragment to be communicated
- * to the activity and potentially other fragments contained in that
- * activity.
- * <p>
- * See the Android Training lesson <a href=
- * "http://developer.android.com/training/basics/fragments/communicating.html"
- * >Communicating with Other Fragments</a> for more information.
- */
- public interface OnNewTransactionFragmentInteractionListener {
- void onTransactionSave(LedgerTransaction tr);
- }
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.annotation.SuppressLint;
-import android.graphics.Typeface;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.inputmethod.EditorInfo;
-import android.widget.AutoCompleteTextView;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.lifecycle.Observer;
-import androidx.recyclerview.widget.RecyclerView;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
-import net.ktnx.mobileledger.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
-import net.ktnx.mobileledger.ui.DatePickerFragment;
-import net.ktnx.mobileledger.ui.TextViewClearHelper;
-import net.ktnx.mobileledger.utils.DimensionUtils;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import java.text.DecimalFormatSymbols;
-import java.text.ParseException;
-import java.util.Date;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.ui.activity.NewTransactionModel.ItemType;
-
-class NewTransactionItemHolder extends RecyclerView.ViewHolder
- implements DatePickerFragment.DatePickedListener, DescriptionSelectedCallback {
- private final String decimalDot;
- private final TextView tvCurrency;
- private final Observer<Boolean> showCommentsObserver;
- private final TextView tvTransactionComment;
- private final TextView tvDate;
- private final AutoCompleteTextView tvDescription;
- private final TextView tvDummy;
- private final AutoCompleteTextView tvAccount;
- private final TextView tvComment;
- private final EditText tvAmount;
- private final ViewGroup lHead;
- private final ViewGroup lAccount;
- private final FrameLayout lPadding;
- private final MobileLedgerProfile mProfile;
- private final Observer<SimpleDate> dateObserver;
- private final Observer<String> descriptionObserver;
- private final Observer<String> transactionCommentObserver;
- private final Observer<String> hintObserver;
- private final Observer<Integer> focusedAccountObserver;
- private final Observer<Integer> accountCountObserver;
- private final Observer<Boolean> editableObserver;
- private final Observer<Currency.Position> currencyPositionObserver;
- private final Observer<Boolean> currencyGapObserver;
- private final Observer<Locale> localeObserver;
- private final Observer<Currency> currencyObserver;
- private final Observer<Boolean> showCurrencyObserver;
- private final Observer<String> commentObserver;
- private final Observer<Boolean> amountValidityObserver;
- private String decimalSeparator;
- private NewTransactionModel.Item item;
- private Date date;
- private boolean inUpdate = false;
- private boolean syncingData = false;
- //TODO multiple amounts with different currencies per posting
- NewTransactionItemHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) {
- super(itemView);
- lAccount = itemView.findViewById(R.id.ntr_account);
- tvAccount = lAccount.findViewById(R.id.account_row_acc_name);
- tvComment = lAccount.findViewById(R.id.comment);
- tvTransactionComment = itemView.findViewById(R.id.transaction_comment);
- new TextViewClearHelper().attachToTextView((EditText) tvComment);
- tvAmount = itemView.findViewById(R.id.account_row_acc_amounts);
- tvCurrency = itemView.findViewById(R.id.currency);
- tvDate = itemView.findViewById(R.id.new_transaction_date);
- tvDescription = itemView.findViewById(R.id.new_transaction_description);
- tvDummy = itemView.findViewById(R.id.dummy_text);
- lHead = itemView.findViewById(R.id.ntr_data);
- lPadding = itemView.findViewById(R.id.ntr_padding);
- final View commentLayout = itemView.findViewById(R.id.comment_layout);
- final View transactionCommentLayout =
- itemView.findViewById(R.id.transaction_comment_layout);
-
- tvDescription.setNextFocusForwardId(View.NO_ID);
- tvAccount.setNextFocusForwardId(View.NO_ID);
- tvAmount.setNextFocusForwardId(View.NO_ID); // magic!
-
- tvDate.setOnClickListener(v -> pickTransactionDate());
-
- lAccount.findViewById(R.id.comment_button)
- .setOnClickListener(v -> {
- tvComment.setVisibility(View.VISIBLE);
- tvComment.requestFocus();
- });
-
- transactionCommentLayout.findViewById(R.id.comment_button)
- .setOnClickListener(v -> {
- tvTransactionComment.setVisibility(View.VISIBLE);
- tvTransactionComment.requestFocus();
- });
-
- mProfile = Data.getProfile();
-
- View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
- final int id = v.getId();
- if (hasFocus) {
- boolean wasSyncing = syncingData;
- syncingData = true;
- try {
- final int pos = getAdapterPosition();
- adapter.updateFocusedItem(pos);
- switch (id) {
- case R.id.account_row_acc_name:
- adapter.noteFocusIsOnAccount(pos);
- break;
- case R.id.account_row_acc_amounts:
- adapter.noteFocusIsOnAmount(pos);
- break;
- case R.id.comment:
- adapter.noteFocusIsOnComment(pos);
- break;
- case R.id.transaction_comment:
- adapter.noteFocusIsOnTransactionComment(pos);
- break;
- case R.id.new_transaction_description:
- adapter.noteFocusIsOnDescription(pos);
- break;
- }
- }
- finally {
- syncingData = wasSyncing;
- }
- }
-
- if (id == R.id.comment) {
- commentFocusChanged(tvComment, hasFocus);
- }
- else if (id == R.id.transaction_comment) {
- commentFocusChanged(tvTransactionComment, hasFocus);
- }
- };
-
- tvDescription.setOnFocusChangeListener(focusMonitor);
- tvAccount.setOnFocusChangeListener(focusMonitor);
- tvAmount.setOnFocusChangeListener(focusMonitor);
- tvComment.setOnFocusChangeListener(focusMonitor);
- tvTransactionComment.setOnFocusChangeListener(focusMonitor);
-
- MLDB.hookAutocompletionAdapter(tvDescription.getContext(), tvDescription,
- MLDB.DESCRIPTION_HISTORY_TABLE, "description", false, adapter, mProfile);
- MLDB.hookAutocompletionAdapter(tvAccount.getContext(), tvAccount, MLDB.ACCOUNTS_TABLE,
- "name", true, this, mProfile);
-
- decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance()
- .getMonetaryDecimalSeparator());
- localeObserver = locale -> decimalSeparator = String.valueOf(
- DecimalFormatSymbols.getInstance(locale)
- .getMonetaryDecimalSeparator());
-
- decimalDot = ".";
-
- final TextWatcher tw = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
-
- @Override
- public void afterTextChanged(Editable s) {
-// debug("input", "text changed");
- if (inUpdate)
- return;
-
- Logger.debug("textWatcher", "calling syncData()");
- syncData();
- Logger.debug("textWatcher",
- "syncData() returned, checking if transaction is submittable");
- adapter.checkTransactionSubmittable();
- Logger.debug("textWatcher", "done");
- }
- };
- final TextWatcher amountWatcher = new TextWatcher() {
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- Logger.debug("num",
- String.format(Locale.US, "beforeTextChanged: start=%d, count=%d, after=%d",
- start, count, after));
- }
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
- @Override
- public void afterTextChanged(Editable s) {
-
- if (syncData())
- adapter.checkTransactionSubmittable();
- }
- };
- tvDescription.addTextChangedListener(tw);
- tvTransactionComment.addTextChangedListener(tw);
- tvAccount.addTextChangedListener(tw);
- tvComment.addTextChangedListener(tw);
- tvAmount.addTextChangedListener(amountWatcher);
-
- tvCurrency.setOnClickListener(v -> {
- CurrencySelectorFragment cpf = new CurrencySelectorFragment();
- cpf.showPositionAndPadding();
- cpf.setOnCurrencySelectedListener(c -> item.setCurrency(c));
- final AppCompatActivity activity = (AppCompatActivity) v.getContext();
- cpf.show(activity.getSupportFragmentManager(), "currency-selector");
- });
-
- dateObserver = date -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- tvDate.setText(item.getFormattedDate());
- }
- finally {
- syncingData = false;
- }
- };
- descriptionObserver = description -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- tvDescription.setText(description);
- }
- finally {
- syncingData = false;
- }
- };
- transactionCommentObserver = transactionComment -> {
- final View focusedView = tvTransactionComment.findFocus();
- tvTransactionComment.setTypeface(null,
- (focusedView == tvTransactionComment) ? Typeface.NORMAL : Typeface.ITALIC);
- tvTransactionComment.setVisibility(
- ((focusedView != tvTransactionComment) && TextUtils.isEmpty(transactionComment))
- ? View.INVISIBLE : View.VISIBLE);
-
- };
- hintObserver = hint -> {
- if (syncingData)
- return;
- syncingData = true;
- try {
- if (hint == null)
- tvAmount.setHint(R.string.zero_amount);
- else
- tvAmount.setHint(hint);
- }
- finally {
- syncingData = false;
- }
- };
- editableObserver = this::setEditable;
- commentFocusChanged(tvTransactionComment, false);
- commentFocusChanged(tvComment, false);
- focusedAccountObserver = index -> {
- if ((index == null) || !index.equals(getAdapterPosition()) || itemView.hasFocus())
- return;
-
- switch (item.getType()) {
- case generalData:
- // bad idea - double pop-up, and not really necessary.
- // the user can tap the input to get the calendar
- //if (!tvDate.hasFocus()) tvDate.requestFocus();
- switch (item.getFocusedElement()) {
- case TransactionComment:
- tvTransactionComment.setVisibility(View.VISIBLE);
- tvTransactionComment.requestFocus();
- break;
- case Description:
- boolean focused = tvDescription.requestFocus();
- tvDescription.dismissDropDown();
- if (focused)
- Misc.showSoftKeyboard(
- (NewTransactionActivity) tvDescription.getContext());
- break;
- }
- break;
- case transactionRow:
- switch (item.getFocusedElement()) {
- case Amount:
- tvAmount.requestFocus();
- break;
- case Comment:
- tvComment.setVisibility(View.VISIBLE);
- tvComment.requestFocus();
- break;
- case Account:
- boolean focused = tvAccount.requestFocus();
- tvAccount.dismissDropDown();
- if (focused)
- Misc.showSoftKeyboard(
- (NewTransactionActivity) tvAccount.getContext());
- break;
- }
-
- break;
- }
- };
- accountCountObserver = count -> {
- final int adapterPosition = getAdapterPosition();
- final int layoutPosition = getLayoutPosition();
- Logger.debug("holder",
- String.format(Locale.US, "count=%d; pos=%d, layoutPos=%d [%s]", count,
- adapterPosition, layoutPosition, item.getType()
- .toString()
- .concat(item.getType() ==
- ItemType.transactionRow
- ? String.format(Locale.US,
- "'%s'=%s",
- item.getAccount()
- .getAccountName(),
- item.getAccount()
- .isAmountSet()
- ? String.format(Locale.US,
- "%.2f",
- item.getAccount()
- .getAmount())
- : "unset") : "")));
- if (adapterPosition == count)
- tvAmount.setImeOptions(EditorInfo.IME_ACTION_DONE);
- else
- tvAmount.setImeOptions(EditorInfo.IME_ACTION_NEXT);
- };
-
- currencyObserver = currency -> {
- setCurrency(currency);
- adapter.checkTransactionSubmittable();
- };
-
- currencyGapObserver =
- hasGap -> updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(),
- hasGap);
-
- currencyPositionObserver =
- position -> updateCurrencyPositionAndPadding(position, Data.currencyGap.getValue());
-
- showCurrencyObserver = showCurrency -> {
- if (showCurrency) {
- tvCurrency.setVisibility(View.VISIBLE);
- }
- else {
- tvCurrency.setVisibility(View.GONE);
- item.setCurrency(null);
- }
- };
-
- commentObserver = comment -> {
- final View focusedView = tvComment.findFocus();
- tvComment.setTypeface(null,
- (focusedView == tvComment) ? Typeface.NORMAL : Typeface.ITALIC);
- tvComment.setVisibility(
- ((focusedView != tvComment) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
- : View.VISIBLE);
- };
-
- showCommentsObserver = show -> {
- final View amountLayout = itemView.findViewById(R.id.amount_layout);
- ConstraintLayout.LayoutParams amountLayoutParams =
- (ConstraintLayout.LayoutParams) amountLayout.getLayoutParams();
- ConstraintLayout.LayoutParams accountParams =
- (ConstraintLayout.LayoutParams) tvAccount.getLayoutParams();
- if (show) {
- accountParams.endToStart = ConstraintLayout.LayoutParams.UNSET;
- accountParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
-
- amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
- amountLayoutParams.topToBottom = tvAccount.getId();
-
- commentLayout.setVisibility(View.VISIBLE);
- }
- else {
- accountParams.endToStart = amountLayout.getId();
- accountParams.endToEnd = ConstraintLayout.LayoutParams.UNSET;
-
- amountLayoutParams.topToBottom = ConstraintLayout.LayoutParams.UNSET;
- amountLayoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID;
-
- commentLayout.setVisibility(View.GONE);
- }
-
- tvAccount.setLayoutParams(accountParams);
- amountLayout.setLayoutParams(amountLayoutParams);
-
- transactionCommentLayout.setVisibility(show ? View.VISIBLE : View.GONE);
- };
-
- amountValidityObserver = valid -> {
- tvAmount.setCompoundDrawablesRelativeWithIntrinsicBounds(
- valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
- tvAmount.setMinEms(valid ? 4 : 5);
- };
- }
- private void commentFocusChanged(TextView textView, boolean hasFocus) {
- @ColorInt int textColor;
- textColor = tvDummy.getTextColors()
- .getDefaultColor();
- if (hasFocus) {
- textView.setTypeface(null, Typeface.NORMAL);
- textView.setHint(R.string.transaction_account_comment_hint);
- }
- else {
- int alpha = (textColor >> 24 & 0xff);
- alpha = 3 * alpha / 4;
- textColor = (alpha << 24) | (0x00ffffff & textColor);
- textView.setTypeface(null, Typeface.ITALIC);
- textView.setHint("");
- if (TextUtils.isEmpty(textView.getText())) {
- textView.setVisibility(View.INVISIBLE);
- }
- }
- textView.setTextColor(textColor);
-
- }
- private void updateCurrencyPositionAndPadding(Currency.Position position, boolean hasGap) {
- ConstraintLayout.LayoutParams amountLP =
- (ConstraintLayout.LayoutParams) tvAmount.getLayoutParams();
- ConstraintLayout.LayoutParams currencyLP =
- (ConstraintLayout.LayoutParams) tvCurrency.getLayoutParams();
-
- if (position == Currency.Position.before) {
- currencyLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
- currencyLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
-
- amountLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
- amountLP.endToStart = ConstraintLayout.LayoutParams.UNSET;
- amountLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
- amountLP.startToEnd = tvCurrency.getId();
-
- tvCurrency.setGravity(Gravity.END);
- }
- else {
- currencyLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
- currencyLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
-
- amountLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
- amountLP.startToEnd = ConstraintLayout.LayoutParams.UNSET;
- amountLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
- amountLP.endToStart = tvCurrency.getId();
-
- tvCurrency.setGravity(Gravity.START);
- }
-
- amountLP.resolveLayoutDirection(tvAmount.getLayoutDirection());
- currencyLP.resolveLayoutDirection(tvCurrency.getLayoutDirection());
-
- tvAmount.setLayoutParams(amountLP);
- tvCurrency.setLayoutParams(currencyLP);
-
- // distance between the amount and the currency symbol
- int gapSize = DimensionUtils.sp2px(tvCurrency.getContext(), 5);
-
- if (position == Currency.Position.before) {
- tvCurrency.setPaddingRelative(0, 0, hasGap ? gapSize : 0, 0);
- }
- else {
- tvCurrency.setPaddingRelative(hasGap ? gapSize : 0, 0, 0, 0);
- }
- }
- private void setCurrencyString(String currency) {
- @ColorInt int textColor = tvDummy.getTextColors()
- .getDefaultColor();
- if ((currency == null) || currency.isEmpty()) {
- tvCurrency.setText(R.string.currency_symbol);
- int alpha = (textColor >> 24) & 0xff;
- alpha = alpha * 3 / 4;
- tvCurrency.setTextColor((alpha << 24) | (0x00ffffff & textColor));
- }
- else {
- tvCurrency.setText(currency);
- tvCurrency.setTextColor(textColor);
- }
- }
- private void setCurrency(Currency currency) {
- setCurrencyString((currency == null) ? null : currency.getName());
- }
- private void setEditable(Boolean editable) {
- tvDate.setEnabled(editable);
- tvDescription.setEnabled(editable);
- tvAccount.setEnabled(editable);
- tvAmount.setEnabled(editable);
- }
- private void beginUpdates() {
- if (inUpdate)
- throw new RuntimeException("Already in update mode");
- inUpdate = true;
- }
- private void endUpdates() {
- if (!inUpdate)
- throw new RuntimeException("Not in update mode");
- inUpdate = false;
- }
- /**
- * syncData()
- * <p>
- * Stores the data from the UI elements into the model item
- * Returns true if there were changes made that suggest transaction has to be
- * checked for being submittable
- */
- private boolean syncData() {
- if (item == null)
- return false;
-
- if (syncingData) {
- Logger.debug("new-trans", "skipping syncData() loop");
- return false;
- }
-
- syncingData = true;
-
- try {
- switch (item.getType()) {
- case generalData:
- item.setDate(String.valueOf(tvDate.getText()));
- item.setDescription(String.valueOf(tvDescription.getText()));
- item.setTransactionComment(String.valueOf(tvTransactionComment.getText()));
- break;
- case transactionRow:
- final LedgerTransactionAccount account = item.getAccount();
- account.setAccountName(String.valueOf(tvAccount.getText()));
-
- item.setComment(String.valueOf(tvComment.getText()));
-
- String amount = String.valueOf(tvAmount.getText());
- amount = amount.trim();
-
- if (amount.isEmpty()) {
- account.resetAmount();
- item.validateAmount();
- }
- else {
- try {
- amount = amount.replace(decimalSeparator, decimalDot);
- account.setAmount(Float.parseFloat(amount));
- item.validateAmount();
- }
- catch (NumberFormatException e) {
- Logger.debug("new-trans", String.format(
- "assuming amount is not set due to number format exception. " +
- "input was '%s'", amount));
- account.invalidateAmount();
- item.invalidateAmount();
- }
- final String curr = String.valueOf(tvCurrency.getText());
- if (curr.equals(tvCurrency.getContext()
- .getResources()
- .getString(R.string.currency_symbol)) ||
- curr.isEmpty())
- account.setCurrency(null);
- else
- account.setCurrency(curr);
- }
-
- break;
- case bottomFiller:
- throw new RuntimeException("Should not happen");
- }
-
- return true;
- }
- catch (ParseException e) {
- throw new RuntimeException("Should not happen", e);
- }
- finally {
- syncingData = false;
- }
- }
- private void pickTransactionDate() {
- DatePickerFragment picker = new DatePickerFragment();
- picker.setFutureDates(mProfile.getFutureDates());
- picker.setOnDatePickedListener(this);
- picker.setCurrentDateFromText(tvDate.getText());
- picker.show(((NewTransactionActivity) tvDate.getContext()).getSupportFragmentManager(),
- null);
- }
- /**
- * setData
- *
- * @param item updates the UI elements with the data from the model item
- */
- @SuppressLint("DefaultLocale")
- public void setData(NewTransactionModel.Item item) {
- beginUpdates();
- try {
- if (this.item != null && !this.item.equals(item)) {
- this.item.stopObservingDate(dateObserver);
- this.item.stopObservingDescription(descriptionObserver);
- this.item.stopObservingTransactionComment(transactionCommentObserver);
- this.item.stopObservingAmountHint(hintObserver);
- this.item.stopObservingEditableFlag(editableObserver);
- this.item.getModel()
- .stopObservingFocusedItem(focusedAccountObserver);
- this.item.getModel()
- .stopObservingAccountCount(accountCountObserver);
- Data.currencySymbolPosition.removeObserver(currencyPositionObserver);
- Data.currencyGap.removeObserver(currencyGapObserver);
- Data.locale.removeObserver(localeObserver);
- this.item.stopObservingCurrency(currencyObserver);
- this.item.getModel().showCurrency.removeObserver(showCurrencyObserver);
- this.item.stopObservingComment(commentObserver);
- this.item.getModel().showComments.removeObserver(showCommentsObserver);
- this.item.stopObservingAmountValidity(amountValidityObserver);
-
- this.item = null;
- }
-
- switch (item.getType()) {
- case generalData:
- tvDate.setText(item.getFormattedDate());
- tvDescription.setText(item.getDescription());
- tvTransactionComment.setText(item.getTransactionComment());
- lHead.setVisibility(View.VISIBLE);
- lAccount.setVisibility(View.GONE);
- lPadding.setVisibility(View.GONE);
- setEditable(true);
- break;
- case transactionRow:
- LedgerTransactionAccount acc = item.getAccount();
- tvAccount.setText(acc.getAccountName());
- tvComment.setText(acc.getComment());
- if (acc.isAmountSet()) {
- tvAmount.setText(String.format("%1.2f", acc.getAmount()));
- }
- else {
- tvAmount.setText("");
-// tvAmount.setHint(R.string.zero_amount);
- }
- tvAmount.setHint(item.getAmountHint());
- setCurrencyString(acc.getCurrency());
- lHead.setVisibility(View.GONE);
- lAccount.setVisibility(View.VISIBLE);
- lPadding.setVisibility(View.GONE);
- setEditable(true);
- break;
- case bottomFiller:
- lHead.setVisibility(View.GONE);
- lAccount.setVisibility(View.GONE);
- lPadding.setVisibility(View.VISIBLE);
- setEditable(false);
- break;
- }
- if (this.item == null) { // was null or has changed
- this.item = item;
- final NewTransactionActivity activity =
- (NewTransactionActivity) tvDescription.getContext();
-
- if (!item.isBottomFiller()) {
- item.observeEditableFlag(activity, editableObserver);
- item.getModel()
- .observeFocusedItem(activity, focusedAccountObserver);
- item.getModel()
- .observeShowComments(activity, showCommentsObserver);
- }
- switch (item.getType()) {
- case generalData:
- item.observeDate(activity, dateObserver);
- item.observeDescription(activity, descriptionObserver);
- item.observeTransactionComment(activity, transactionCommentObserver);
- break;
- case transactionRow:
- item.observeAmountHint(activity, hintObserver);
- Data.currencySymbolPosition.observe(activity, currencyPositionObserver);
- Data.currencyGap.observe(activity, currencyGapObserver);
- Data.locale.observe(activity, localeObserver);
- item.observeCurrency(activity, currencyObserver);
- item.getModel().showCurrency.observe(activity, showCurrencyObserver);
- item.observeComment(activity, commentObserver);
- item.getModel()
- .observeAccountCount(activity, accountCountObserver);
- item.observeAmountValidity(activity, amountValidityObserver);
- break;
- }
- }
- }
- finally {
- endUpdates();
- }
- }
- @Override
- public void onDatePicked(int year, int month, int day) {
- item.setDate(new SimpleDate(year, month + 1, day));
- boolean focused = tvDescription.requestFocus();
- if (focused)
- Misc.showSoftKeyboard((NewTransactionActivity) tvAccount.getContext());
-
- }
- @Override
- public void descriptionSelected(String description) {
- tvAccount.setText(description);
- tvAmount.requestFocus(View.FOCUS_FORWARD);
- }
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.database.Cursor;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.snackbar.Snackbar;
-
-import net.ktnx.mobileledger.BuildConfig;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
-import net.ktnx.mobileledger.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.Misc;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
- implements DescriptionSelectedCallback {
- private final NewTransactionModel model;
- private MobileLedgerProfile mProfile;
- private final ItemTouchHelper touchHelper;
- private RecyclerView recyclerView;
- private int checkHoldCounter = 0;
- NewTransactionItemsAdapter(NewTransactionModel viewModel, MobileLedgerProfile profile) {
- super();
- model = viewModel;
- mProfile = profile;
- int size = model.getAccountCount();
- while (size < 2) {
- Logger.debug("new-transaction",
- String.format(Locale.US, "%d accounts is too little, Calling addRow()", size));
- size = addRow();
- }
-
- NewTransactionItemsAdapter adapter = this;
-
- touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
- @Override
- public boolean isLongPressDragEnabled() {
- return true;
- }
- @Override
- public boolean canDropOver(@NonNull RecyclerView recyclerView,
- @NonNull RecyclerView.ViewHolder current,
- @NonNull RecyclerView.ViewHolder target) {
- final int adapterPosition = target.getAdapterPosition();
-
- // first and last items are immovable
- if (adapterPosition == 0)
- return false;
- if (adapterPosition == adapter.getItemCount() - 1)
- return false;
-
- return super.canDropOver(recyclerView, current, target);
- }
- @Override
- public int getMovementFlags(@NonNull RecyclerView recyclerView,
- @NonNull RecyclerView.ViewHolder viewHolder) {
- int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
- // the top (date and description) and the bottom (padding) items are always there
- final int adapterPosition = viewHolder.getAdapterPosition();
- if ((adapterPosition > 0) && (adapterPosition < adapter.getItemCount() - 1)) {
- flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
- ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
- makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
- ItemTouchHelper.START | ItemTouchHelper.END);
- }
-
- return flags;
- }
- @Override
- public boolean onMove(@NonNull RecyclerView recyclerView,
- @NonNull RecyclerView.ViewHolder viewHolder,
- @NonNull RecyclerView.ViewHolder target) {
-
- model.swapItems(viewHolder.getAdapterPosition(), target.getAdapterPosition());
- notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
- return true;
- }
- @Override
- public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
- int pos = viewHolder.getAdapterPosition();
- viewModel.removeItem(pos - 1);
- notifyItemRemoved(pos);
- viewModel.sendCountNotifications(); // needed after items re-arrangement
- checkTransactionSubmittable();
- }
- });
- }
- public void setProfile(MobileLedgerProfile profile) {
- mProfile = profile;
- }
- private int addRow() {
- return addRow(null);
- }
- private int addRow(String commodity) {
- final int newAccountCount = model.addAccount(new LedgerTransactionAccount("", commodity));
- Logger.debug("new-transaction",
- String.format(Locale.US, "invoking notifyItemInserted(%d)", newAccountCount));
- // the header is at position 0
- notifyItemInserted(newAccountCount);
- model.sendCountNotifications(); // needed after holders' positions have changed
- return newAccountCount;
- }
- @NonNull
- @Override
- public NewTransactionItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
- LinearLayout row = (LinearLayout) LayoutInflater.from(parent.getContext())
- .inflate(R.layout.new_transaction_row,
- parent, false);
-
- return new NewTransactionItemHolder(row, this);
- }
- @Override
- public void onBindViewHolder(@NonNull NewTransactionItemHolder holder, int position) {
- Logger.debug("bind", String.format(Locale.US, "Binding item at position %d", position));
- NewTransactionModel.Item item = model.getItem(position);
- holder.setData(item);
- Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
- .toString(),
- position));
- }
- @Override
- public int getItemCount() {
- return model.getAccountCount() + 2;
- }
- private boolean accountListIsEmpty() {
- for (int i = 0; i < model.getAccountCount(); i++) {
- LedgerTransactionAccount acc = model.getAccount(i);
- if (!acc.getAccountName()
- .isEmpty())
- return false;
- if (acc.isAmountSet())
- return false;
- }
-
- return true;
- }
- @Override
- public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
- super.onAttachedToRecyclerView(recyclerView);
- this.recyclerView = recyclerView;
- touchHelper.attachToRecyclerView(recyclerView);
- }
- @Override
- public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
- touchHelper.attachToRecyclerView(null);
- super.onDetachedFromRecyclerView(recyclerView);
- this.recyclerView = null;
- }
- public void descriptionSelected(String description) {
- debug("description selected", description);
- if (!accountListIsEmpty())
- return;
-
- String accFilter = mProfile.getPreferredAccountsFilter();
-
- ArrayList<String> params = new ArrayList<>();
- StringBuilder sb = new StringBuilder("select t.profile, t.id from transactions t");
-
- if (!TextUtils.isEmpty(accFilter)) {
- sb.append(" JOIN transaction_accounts ta")
- .append(" ON ta.profile = t.profile")
- .append(" AND ta.transaction_id = t.id");
- }
-
- sb.append(" WHERE t.description=?");
- params.add(description);
-
- if (!TextUtils.isEmpty(accFilter)) {
- sb.append(" AND ta.account_name LIKE '%'||?||'%'");
- params.add(accFilter);
- }
-
- sb.append(" ORDER BY t.year desc, t.month desc, t.day desc LIMIT 1");
-
- final String sql = sb.toString();
- debug("description", sql);
- debug("description", params.toString());
-
- Activity activity = (Activity) recyclerView.getContext();
- // FIXME: handle exceptions?
- MLDB.queryInBackground(sql, params.toArray(new String[]{}), new MLDB.CallbackHelper() {
- @Override
- public void onStart() {
- model.incrementBusyCounter();
- }
- @Override
- public void onDone() {
- model.decrementBusyCounter();
- }
- @Override
- public boolean onRow(@NonNull Cursor cursor) {
- final String profileUUID = cursor.getString(0);
- final int transactionId = cursor.getInt(1);
- activity.runOnUiThread(() -> loadTransactionIntoModel(profileUUID, transactionId));
- return false; // limit 1, by the way
- }
- @Override
- public void onNoRows() {
- if (TextUtils.isEmpty(accFilter))
- return;
-
- debug("description", "Trying transaction search without preferred account filter");
-
- final String broaderSql =
- "select t.profile, t.id from transactions t where t.description=?" +
- " ORDER BY year desc, month desc, day desc LIMIT 1";
- params.remove(1);
- debug("description", broaderSql);
- debug("description", description);
-
- activity.runOnUiThread(
- () -> Snackbar.make(recyclerView, R.string.ignoring_preferred_account,
- Snackbar.LENGTH_LONG)
- .show());
-
- MLDB.queryInBackground(broaderSql, new String[]{description},
- new MLDB.CallbackHelper() {
- @Override
- public void onStart() {
- model.incrementBusyCounter();
- }
- @Override
- public boolean onRow(@NonNull Cursor cursor) {
- final String profileUUID = cursor.getString(0);
- final int transactionId = cursor.getInt(1);
- activity.runOnUiThread(
- () -> loadTransactionIntoModel(profileUUID, transactionId));
- return false;
- }
- @Override
- public void onDone() {
- model.decrementBusyCounter();
- }
- });
- }
- });
- }
- private void loadTransactionIntoModel(String profileUUID, int transactionId) {
- LedgerTransaction tr;
- MobileLedgerProfile profile = Data.getProfile(profileUUID);
- if (profile == null)
- throw new RuntimeException(String.format(
- "Unable to find profile %s, which is supposed to contain transaction %d",
- profileUUID, transactionId));
-
- tr = profile.loadTransaction(transactionId);
- List<LedgerTransactionAccount> accounts = tr.getAccounts();
- NewTransactionModel.Item firstNegative = null;
- NewTransactionModel.Item firstPositive = null;
- int singleNegativeIndex = -1;
- int singlePositiveIndex = -1;
- int negativeCount = 0;
- for (int i = 0; i < accounts.size(); i++) {
- LedgerTransactionAccount acc = accounts.get(i);
- NewTransactionModel.Item item;
- if (model.getAccountCount() < i + 1) {
- model.addAccount(acc);
- notifyItemInserted(i + 1);
- }
- item = model.getItem(i + 1);
-
- item.getAccount()
- .setAccountName(acc.getAccountName());
- item.setComment(acc.getComment());
- if (acc.isAmountSet()) {
- item.getAccount()
- .setAmount(acc.getAmount());
- if (acc.getAmount() < 0) {
- if (firstNegative == null) {
- firstNegative = item;
- singleNegativeIndex = i;
- }
- else
- singleNegativeIndex = -1;
- }
- else {
- if (firstPositive == null) {
- firstPositive = item;
- singlePositiveIndex = i;
- }
- else
- singlePositiveIndex = -1;
- }
- }
- else
- item.getAccount()
- .resetAmount();
- notifyItemChanged(i + 1);
- }
-
- if (singleNegativeIndex != -1) {
- firstNegative.getAccount()
- .resetAmount();
- model.moveItemLast(singleNegativeIndex);
- }
- else if (singlePositiveIndex != -1) {
- firstPositive.getAccount()
- .resetAmount();
- model.moveItemLast(singlePositiveIndex);
- }
-
- checkTransactionSubmittable();
- model.setFocusedItem(1);
- }
- public void toggleAllEditing(boolean editable) {
- // item 0 is the header
- for (int i = 0; i <= model.getAccountCount(); i++) {
- model.getItem(i)
- .setEditable(editable);
- notifyItemChanged(i);
- // TODO perhaps do only one notification about the whole range (notifyDatasetChanged)?
- }
- }
- void reset() {
- int presentItemCount = model.getAccountCount();
- model.reset();
- notifyItemChanged(0); // header changed
- notifyItemRangeChanged(1, 2); // the two empty rows
- if (presentItemCount > 2)
- notifyItemRangeRemoved(3, presentItemCount - 2); // all the rest are gone
- }
- void updateFocusedItem(int position) {
- model.updateFocusedItem(position);
- }
- void noteFocusIsOnAccount(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Account);
- }
- void noteFocusIsOnAmount(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Amount);
- }
- void noteFocusIsOnComment(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.Comment);
- }
- void noteFocusIsOnTransactionComment(int position) {
- model.noteFocusChanged(position, NewTransactionModel.FocusedElement.TransactionComment);
- }
- public void noteFocusIsOnDescription(int pos) {
- model.noteFocusChanged(pos, NewTransactionModel.FocusedElement.Description);
- }
- private void holdSubmittableChecks() {
- checkHoldCounter++;
- }
- private void releaseSubmittableChecks() {
- if (checkHoldCounter == 0)
- throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
- checkHoldCounter--;
- }
- void setItemCurrency(NewTransactionModel.Item item, Currency newCurrency) {
- Currency oldCurrency = item.getCurrency();
- if (!Currency.equal(newCurrency, oldCurrency)) {
- holdSubmittableChecks();
- try {
- item.setCurrency(newCurrency);
-// for (Item i : items) {
-// if (Currency.equal(i.getCurrency(), oldCurrency))
-// i.setCurrency(newCurrency);
-// }
- }
- finally {
- releaseSubmittableChecks();
- }
-
- checkTransactionSubmittable();
- }
- }
- /*
- A transaction is submittable if:
- 0) has description
- 1) has at least two account names
- 2) each row with amount has account name
- 3) for each commodity:
- 3a) amounts must balance to 0, or
- 3b) there must be exactly one empty amount (with account)
- 4) empty accounts with empty amounts are ignored
- Side effects:
- 5) a row with an empty account name or empty amount is guaranteed to exist for each
- commodity
- 6) at least two rows need to be present in the ledger
-
- */
- @SuppressLint("DefaultLocale")
- void checkTransactionSubmittable() {
- if (checkHoldCounter > 0)
- return;
-
- int accounts = 0;
- final BalanceForCurrency balance = new BalanceForCurrency();
- final String descriptionText = model.getDescription();
- boolean submittable = true;
- final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
- final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
- final List<NewTransactionModel.Item> emptyRows = new ArrayList<>();
-
- try {
- if ((descriptionText == null) || descriptionText.trim()
- .isEmpty())
- {
- Logger.debug("submittable", "Transaction not submittable: missing description");
- submittable = false;
- }
-
- for (int i = 0; i < model.items.size(); i++) {
- NewTransactionModel.Item item = model.items.get(i);
-
- LedgerTransactionAccount acc = item.getAccount();
- String acc_name = acc.getAccountName()
- .trim();
- String currName = acc.getCurrency();
-
- itemsForCurrency.add(currName, item);
-
- if (acc_name.isEmpty()) {
- itemsWithEmptyAccountForCurrency.add(currName, item);
-
- if (acc.isAmountSet()) {
- // 2) each amount has account name
- Logger.debug("submittable", String.format(
- "Transaction not submittable: row %d has no account name, but" +
- " has" + " amount %1.2f", i + 1, acc.getAmount()));
- submittable = false;
- }
- else {
- emptyRowsForCurrency.add(currName, item);
- }
- }
- else {
- accounts++;
- itemsWithAccountForCurrency.add(currName, item);
- }
-
- if (!acc.isAmountValid()) {
- Logger.debug("submittable",
- String.format("Not submittable: row %d has an invalid amount", i + 1));
- submittable = false;
- }
- else if (acc.isAmountSet()) {
- itemsWithAmountForCurrency.add(currName, item);
- balance.add(currName, acc.getAmount());
- }
- else {
- itemsWithEmptyAmountForCurrency.add(currName, item);
-
- if (!acc_name.isEmpty())
- itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
- }
- }
-
- // 1) has at least two account names
- if (accounts < 2) {
- if (accounts == 0)
- Logger.debug("submittable",
- "Transaction not submittable: no account " + "names");
- else if (accounts == 1)
- Logger.debug("submittable",
- "Transaction not submittable: only one account name");
- else
- Logger.debug("submittable",
- String.format("Transaction not submittable: only %d account names",
- accounts));
- submittable = false;
- }
-
- // 3) for each commodity:
- // 3a) amount must balance to 0, or
- // 3b) there must be exactly one empty amount (with account)
- for (String balCurrency : itemsForCurrency.currencies()) {
- float currencyBalance = balance.get(balCurrency);
- if (Misc.isZero(currencyBalance)) {
- // remove hints from all amount inputs in that currency
- for (NewTransactionModel.Item item : model.items) {
- if (Currency.equal(item.getCurrency(), balCurrency))
- item.setAmountHint(null);
- }
- }
- else {
- List<NewTransactionModel.Item> list =
- itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
- int balanceReceiversCount = list.size();
- if (balanceReceiversCount != 1) {
- if (BuildConfig.DEBUG) {
- if (balanceReceiversCount == 0)
- Logger.debug("submittable", String.format(
- "Transaction not submittable [%s]: non-zero balance " +
- "with no empty amounts with accounts", balCurrency));
- else
- Logger.debug("submittable", String.format(
- "Transaction not submittable [%s]: non-zero balance " +
- "with multiple empty amounts with accounts", balCurrency));
- }
- submittable = false;
- }
-
- List<NewTransactionModel.Item> emptyAmountList =
- itemsWithEmptyAmountForCurrency.getList(balCurrency);
-
- // suggest off-balance amount to a row and remove hints on other rows
- NewTransactionModel.Item receiver = null;
- if (!list.isEmpty())
- receiver = list.get(0);
- else if (!emptyAmountList.isEmpty())
- receiver = emptyAmountList.get(0);
-
- for (NewTransactionModel.Item item : model.items) {
- if (!Currency.equal(item.getCurrency(), balCurrency))
- continue;
-
- if (item.equals(receiver)) {
- if (BuildConfig.DEBUG)
- Logger.debug("submittable",
- String.format("Setting amount hint to %1.2f [%s]",
- -currencyBalance, balCurrency));
- item.setAmountHint(String.format("%1.2f", -currencyBalance));
- }
- else {
- if (BuildConfig.DEBUG)
- Logger.debug("submittable",
- String.format("Resetting hint of '%s' [%s]",
- (item.getAccount() == null) ? "" : item.getAccount()
- .getAccountName(),
- balCurrency));
- item.setAmountHint(null);
- }
- }
- }
- }
-
- // 5) a row with an empty account name or empty amount is guaranteed to exist for
- // each commodity
- for (String balCurrency : balance.currencies()) {
- int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
- int currRows = itemsForCurrency.size(balCurrency);
- int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
- int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
- if ((currEmptyRows == 0) &&
- ((currRows == currAccounts) || (currRows == currAmounts)))
- {
- // perhaps there already is an unused empty row for another currency that
- // is not used?
-// boolean foundIt = false;
-// for (Item item : emptyRows) {
-// Currency itemCurrency = item.getCurrency();
-// String itemCurrencyName =
-// (itemCurrency == null) ? "" : itemCurrency.getName();
-// if (Misc.isZero(balance.get(itemCurrencyName))) {
-// item.setCurrency(Currency.loadByName(balCurrency));
-// item.setAmountHint(
-// String.format("%1.2f", -balance.get(balCurrency)));
-// foundIt = true;
-// break;
-// }
-// }
-//
-// if (!foundIt)
- addRow(balCurrency);
- }
- }
-
- // drop extra empty rows, not needed
- for (String currName : emptyRowsForCurrency.currencies()) {
- List<NewTransactionModel.Item> emptyItems = emptyRowsForCurrency.getList(currName);
- while ((model.items.size() > 2) && (emptyItems.size() > 1)) {
- NewTransactionModel.Item item = emptyItems.get(1);
- emptyItems.remove(1);
- model.removeRow(item, this);
- }
-
- // unused currency, remove last item (which is also an empty one)
- if ((model.items.size() > 2) && (emptyItems.size() == 1)) {
- List<NewTransactionModel.Item> currItems = itemsForCurrency.getList(currName);
-
- if (currItems.size() == 1) {
- NewTransactionModel.Item item = emptyItems.get(0);
- model.removeRow(item, this);
- }
- }
- }
-
- // 6) at least two rows need to be present in the ledger
- while (model.items.size() < 2)
- addRow();
-
-
- debug("submittable", submittable ? "YES" : "NO");
- model.isSubmittable.setValue(submittable);
-
- if (BuildConfig.DEBUG) {
- debug("submittable", "== Dump of all items");
- for (int i = 0; i < model.items.size(); i++) {
- NewTransactionModel.Item item = model.items.get(i);
- LedgerTransactionAccount acc = item.getAccount();
- debug("submittable", String.format("Item %2d: [%4.2f(%s) %s] %s ; %s", i,
- acc.isAmountSet() ? acc.getAmount() : 0,
- item.isAmountHintSet() ? item.getAmountHint() : "ø", acc.getCurrency(),
- acc.getAccountName(), acc.getComment()));
- }
- }
- }
- catch (NumberFormatException e) {
- debug("submittable", "NO (because of NumberFormatException)");
- model.isSubmittable.setValue(false);
- }
- catch (Exception e) {
- e.printStackTrace();
- debug("submittable", "NO (because of an Exception)");
- model.isSubmittable.setValue(false);
- }
- }
-
- private static class BalanceForCurrency {
- private final HashMap<String, Float> hashMap = new HashMap<>();
- float get(String currencyName) {
- Float f = hashMap.get(currencyName);
- if (f == null) {
- f = 0f;
- hashMap.put(currencyName, f);
- }
- return f;
- }
- void add(String currencyName, float amount) {
- hashMap.put(currencyName, get(currencyName) + amount);
- }
- Set<String> currencies() {
- return hashMap.keySet();
- }
- boolean containsCurrency(String currencyName) {
- return hashMap.containsKey(currencyName);
- }
- }
-
- private static class ItemsForCurrency {
- private final HashMap<String, List<NewTransactionModel.Item>> hashMap = new HashMap<>();
- @NonNull
- List<NewTransactionModel.Item> getList(@Nullable String currencyName) {
- List<NewTransactionModel.Item> list = hashMap.get(currencyName);
- if (list == null) {
- list = new ArrayList<>();
- hashMap.put(currencyName, list);
- }
- return list;
- }
- void add(@Nullable String currencyName, @NonNull NewTransactionModel.Item item) {
- getList(currencyName).add(item);
- }
- int size(@Nullable String currencyName) {
- return this.getList(currencyName)
- .size();
- }
- Set<String> currencies() {
- return hashMap.keySet();
- }
- }
-}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.Observer;
-import androidx.lifecycle.ViewModel;
-
-import net.ktnx.mobileledger.model.Currency;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.utils.Globals;
-import net.ktnx.mobileledger.utils.SimpleDate;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class NewTransactionModel extends ViewModel {
- final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
- final ArrayList<Item> items = new ArrayList<>();
- final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
- final MutableLiveData<Boolean> showComments = new MutableLiveData<>(true);
- private final Item header = new Item(this, "");
- private final Item trailer = new Item(this);
- private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
- private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
- private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
- private final AtomicInteger busyCounter = new AtomicInteger(0);
- private final MutableLiveData<Boolean> busyFlag = new MutableLiveData<>(false);
- private final Observer<MobileLedgerProfile> profileObserver = profile -> {
- showCurrency.postValue(profile.getShowCommodityByDefault());
- showComments.postValue(profile.getShowCommentsByDefault());
- };
- private boolean observingDataProfile;
- void observeShowComments(LifecycleOwner owner, Observer<? super Boolean> observer) {
- showComments.observe(owner, observer);
- }
- void observeBusyFlag(@NonNull LifecycleOwner owner, Observer<? super Boolean> observer) {
- busyFlag.observe(owner, observer);
- }
- void observeDataProfile(LifecycleOwner activity) {
- if (!observingDataProfile)
- Data.observeProfile(activity, profileObserver);
- observingDataProfile = true;
- }
- boolean getSimulateSave() {
- return simulateSave.getValue();
- }
- public void setSimulateSave(boolean simulateSave) {
- this.simulateSave.setValue(simulateSave);
- }
- void toggleSimulateSave() {
- simulateSave.setValue(!simulateSave.getValue());
- }
- void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Boolean> observer) {
- this.simulateSave.observe(owner, observer);
- }
- int getAccountCount() {
- return items.size();
- }
- public SimpleDate getDate() {
- return header.date.getValue();
- }
- public String getDescription() {
- return header.description.getValue();
- }
- public String getComment() {
- return header.comment.getValue();
- }
- LiveData<Boolean> isSubmittable() {
- return this.isSubmittable;
- }
- void reset() {
- header.date.setValue(null);
- header.description.setValue(null);
- header.comment.setValue(null);
- items.clear();
- items.add(new Item(this, new LedgerTransactionAccount("")));
- items.add(new Item(this, new LedgerTransactionAccount("")));
- focusedItem.setValue(0);
- }
- void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.focusedItem.observe(owner, observer);
- }
- void stopObservingFocusedItem(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.focusedItem.removeObserver(observer);
- }
- void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.accountCount.observe(owner, observer);
- }
- void stopObservingAccountCount(@NonNull androidx.lifecycle.Observer<? super Integer> observer) {
- this.accountCount.removeObserver(observer);
- }
- int getFocusedItem() { return focusedItem.getValue(); }
- void setFocusedItem(int position) {
- focusedItem.setValue(position);
- }
- int addAccount(LedgerTransactionAccount acc) {
- items.add(new Item(this, acc));
- accountCount.setValue(getAccountCount());
- return items.size();
- }
- boolean accountsInInitialState() {
- for (Item item : items) {
- LedgerTransactionAccount acc = item.getAccount();
- if (acc.isAmountSet())
- return false;
- if (!acc.getAccountName()
- .trim()
- .isEmpty())
- return false;
- }
-
- return true;
- }
- LedgerTransactionAccount getAccount(int index) {
- return items.get(index)
- .getAccount();
- }
- Item getItem(int index) {
- if (index == 0) {
- return header;
- }
-
- if (index <= items.size())
- return items.get(index - 1);
-
- return trailer;
- }
- void removeRow(Item item, NewTransactionItemsAdapter adapter) {
- int pos = items.indexOf(item);
- items.remove(pos);
- if (adapter != null) {
- adapter.notifyItemRemoved(pos + 1);
- sendCountNotifications();
- }
- }
- void removeItem(int pos) {
- items.remove(pos);
- accountCount.setValue(getAccountCount());
- }
- void sendCountNotifications() {
- accountCount.setValue(getAccountCount());
- }
- public void sendFocusedNotification() {
- focusedItem.setValue(focusedItem.getValue());
- }
- void updateFocusedItem(int position) {
- focusedItem.setValue(position);
- }
- void noteFocusChanged(int position, FocusedElement element) {
- getItem(position).setFocusedElement(element);
- }
- void swapItems(int one, int two) {
- Collections.swap(items, one - 1, two - 1);
- }
- void moveItemLast(int index) {
- /* 0
- 1 <-- index
- 2
- 3 <-- desired position
- */
- int itemCount = items.size();
-
- if (index < itemCount - 1) {
- Item acc = items.remove(index);
- items.add(itemCount - 1, acc);
- }
- }
- void toggleCurrencyVisible() {
- showCurrency.setValue(!showCurrency.getValue());
- }
- void stopObservingBusyFlag(Observer<Boolean> observer) {
- busyFlag.removeObserver(observer);
- }
- void incrementBusyCounter() {
- int newValue = busyCounter.incrementAndGet();
- if (newValue == 1)
- busyFlag.postValue(true);
- }
- void decrementBusyCounter() {
- int newValue = busyCounter.decrementAndGet();
- if (newValue == 0)
- busyFlag.postValue(false);
- }
- public boolean getBusyFlag() {
- return busyFlag.getValue();
- }
- public void toggleShowComments() {
- showComments.setValue(!showComments.getValue());
- }
- enum ItemType {generalData, transactionRow, bottomFiller}
-
- enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
-
-
- //==========================================================================================
-
-
- static class Item {
- private final ItemType type;
- private final MutableLiveData<SimpleDate> date = new MutableLiveData<>();
- private final MutableLiveData<String> description = new MutableLiveData<>();
- private final MutableLiveData<String> amountHint = new MutableLiveData<>(null);
- private final NewTransactionModel model;
- private final MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
- private final MutableLiveData<String> comment = new MutableLiveData<>(null);
- private final MutableLiveData<Currency> currency = new MutableLiveData<>(null);
- private final MutableLiveData<Boolean> amountValid = new MutableLiveData<>(true);
- private LedgerTransactionAccount account;
- private FocusedElement focusedElement = FocusedElement.Account;
- private boolean amountHintIsSet = false;
- Item(NewTransactionModel model) {
- this.model = model;
- type = ItemType.bottomFiller;
- editable.setValue(false);
- }
- Item(NewTransactionModel model, String description) {
- this.model = model;
- this.type = ItemType.generalData;
- this.description.setValue(description);
- this.editable.setValue(true);
- }
- Item(NewTransactionModel model, LedgerTransactionAccount account) {
- this.model = model;
- this.type = ItemType.transactionRow;
- this.account = account;
- String currName = account.getCurrency();
- Currency curr = null;
- if ((currName != null) && !currName.isEmpty())
- curr = Currency.loadByName(currName);
- this.currency.setValue(curr);
- this.editable.setValue(true);
- }
- FocusedElement getFocusedElement() {
- return focusedElement;
- }
- void setFocusedElement(FocusedElement focusedElement) {
- this.focusedElement = focusedElement;
- }
- public NewTransactionModel getModel() {
- return model;
- }
- void setEditable(boolean editable) {
- ensureTypeIsGeneralDataOrTransactionRow();
- this.editable.setValue(editable);
- }
- private void ensureTypeIsGeneralDataOrTransactionRow() {
- if ((type != ItemType.generalData) && (type != ItemType.transactionRow)) {
- throw new RuntimeException(
- String.format("Actual type (%s) differs from wanted (%s or %s)", type,
- ItemType.generalData, ItemType.transactionRow));
- }
- }
- String getAmountHint() {
- ensureType(ItemType.transactionRow);
- return amountHint.getValue();
- }
- void setAmountHint(String amountHint) {
- ensureType(ItemType.transactionRow);
-
- // avoid unnecessary triggers
- if (amountHint == null) {
- if (this.amountHint.getValue() == null)
- return;
- amountHintIsSet = false;
- }
- else {
- if (amountHint.equals(this.amountHint.getValue()))
- return;
- amountHintIsSet = true;
- }
-
- this.amountHint.setValue(amountHint);
- }
- void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.amountHint.observe(owner, observer);
- }
- void stopObservingAmountHint(
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.amountHint.removeObserver(observer);
- }
- ItemType getType() {
- return type;
- }
- void ensureType(ItemType wantedType) {
- if (type != wantedType) {
- throw new RuntimeException(
- String.format("Actual type (%s) differs from wanted (%s)", type,
- wantedType));
- }
- }
- public SimpleDate getDate() {
- ensureType(ItemType.generalData);
- return date.getValue();
- }
- public void setDate(SimpleDate date) {
- ensureType(ItemType.generalData);
- this.date.setValue(date);
- }
- public void setDate(String text) throws ParseException {
- if ((text == null) || text.trim()
- .isEmpty())
- {
- setDate((SimpleDate) null);
- return;
- }
-
- SimpleDate date = Globals.parseLedgerDate(text);
- this.setDate(date);
- }
- void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
- this.date.observe(owner, observer);
- }
- void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super SimpleDate> observer) {
- this.date.removeObserver(observer);
- }
- public String getDescription() {
- ensureType(ItemType.generalData);
- return description.getValue();
- }
- public void setDescription(String description) {
- ensureType(ItemType.generalData);
- this.description.setValue(description);
- }
- void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.description.observe(owner, observer);
- }
- void stopObservingDescription(
- @NonNull androidx.lifecycle.Observer<? super String> observer) {
- this.description.removeObserver(observer);
- }
- public String getTransactionComment() {
- ensureType(ItemType.generalData);
- return comment.getValue();
- }
- public void setTransactionComment(String transactionComment) {
- ensureType(ItemType.generalData);
- this.comment.setValue(transactionComment);
- }
- void observeTransactionComment(@NonNull @NotNull LifecycleOwner owner,
- @NonNull Observer<? super String> observer) {
- ensureType(ItemType.generalData);
- this.comment.observe(owner, observer);
- }
- void stopObservingTransactionComment(@NonNull Observer<? super String> observer) {
- this.comment.removeObserver(observer);
- }
- public LedgerTransactionAccount getAccount() {
- ensureType(ItemType.transactionRow);
- return account;
- }
- public void setAccountName(String name) {
- account.setAccountName(name);
- }
- /**
- * getFormattedDate()
- *
- * @return nicely formatted, shortest available date representation
- */
- String getFormattedDate() {
- if (date == null)
- return null;
- SimpleDate d = date.getValue();
- if (d == null)
- return null;
-
- Calendar today = GregorianCalendar.getInstance();
-
- if (today.get(Calendar.YEAR) != d.year) {
- return String.format(Locale.US, "%d/%02d/%02d", d.year, d.month, d.day);
- }
-
- if (today.get(Calendar.MONTH) != d.month - 1) {
- return String.format(Locale.US, "%d/%02d", d.month, d.day);
- }
-
- return String.valueOf(d.day);
- }
- void observeEditableFlag(NewTransactionActivity activity, Observer<Boolean> observer) {
- editable.observe(activity, observer);
- }
- void stopObservingEditableFlag(Observer<Boolean> observer) {
- editable.removeObserver(observer);
- }
- void observeComment(NewTransactionActivity activity, Observer<String> observer) {
- comment.observe(activity, observer);
- }
- void stopObservingComment(Observer<String> observer) {
- comment.removeObserver(observer);
- }
- public void setComment(String comment) {
- getAccount().setComment(comment);
- this.comment.postValue(comment);
- }
- public Currency getCurrency() {
- return this.currency.getValue();
- }
- public void setCurrency(Currency currency) {
- Currency present = this.currency.getValue();
- if ((currency == null) && (present != null) ||
- (currency != null) && !currency.equals(present))
- {
- getAccount().setCurrency((currency != null && !currency.getName()
- .isEmpty())
- ? currency.getName() : null);
- this.currency.setValue(currency);
- }
- }
- void observeCurrency(NewTransactionActivity activity, Observer<Currency> observer) {
- currency.observe(activity, observer);
- }
- void stopObservingCurrency(Observer<Currency> observer) {
- currency.removeObserver(observer);
- }
- boolean isBottomFiller() {
- return this.type == ItemType.bottomFiller;
- }
- boolean isAmountHintSet() {
- return amountHintIsSet;
- }
- void validateAmount() {
- amountValid.setValue(true);
- }
- void invalidateAmount() {
- amountValid.setValue(false);
- }
- void observeAmountValidity(NewTransactionActivity activity, Observer<Boolean> observer) {
- amountValid.observe(activity, observer);
- }
- void stopObservingAmountValidity(Observer<Boolean> observer) {
- amountValid.removeObserver(observer);
- }
- }
-}
+++ /dev/null
-/*
- * Copyright © 2019 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.activity;
-
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuItem;
-
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.ViewModelProvider;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.profiles.ProfileDetailFragment;
-import net.ktnx.mobileledger.ui.profiles.ProfileDetailModel;
-import net.ktnx.mobileledger.utils.Colors;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-/**
- * An activity representing a single Profile detail screen. This
- * activity is only used on narrow width devices. On tablet-size devices,
- * item details are presented side-by-side with a list of items
- * in a ProfileListActivity (not really).
- */
-public class ProfileDetailActivity extends CrashReportingActivity {
- private MobileLedgerProfile profile = null;
- private ProfileDetailFragment mFragment;
- @NotNull
- private ProfileDetailModel getModel() {
- return new ViewModelProvider(this).get(ProfileDetailModel.class);
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- final int index = getIntent().getIntExtra(ProfileDetailFragment.ARG_ITEM_ID, -1);
-
- if (index != -1) {
- ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
- if (profiles != null) {
- profile = profiles.get(index);
- if (profile == null)
- throw new AssertionError(
- String.format("Can't get profile " + "(index:%d) from the global list",
- index));
-
- debug("profiles", String.format(Locale.ENGLISH, "Editing profile %s (%s); hue=%d",
- profile.getName(), profile.getUuid(), profile.getThemeHue()));
- }
- }
-
- super.onCreate(savedInstanceState);
- int themeHue;
- if (profile != null)
- themeHue = profile.getThemeHue();
- else {
- themeHue = Colors.getNewProfileThemeHue(Data.profiles.getValue());
- }
- Colors.setupTheme(this, themeHue);
- final ProfileDetailModel model = getModel();
- model.initialThemeHue = themeHue;
- setContentView(R.layout.activity_profile_detail);
- Toolbar toolbar = findViewById(R.id.detail_toolbar);
- setSupportActionBar(toolbar);
-
-
- // Show the Up button in the action bar.
- ActionBar actionBar = getSupportActionBar();
- if (actionBar != null) {
- actionBar.setDisplayHomeAsUpEnabled(true);
- }
-
- // savedInstanceState is non-null when there is fragment state
- // saved from previous configurations of this activity
- // (e.g. when rotating the screen from portrait to landscape).
- // In this case, the fragment will automatically be re-added
- // to its container so we don't need to manually add it.
- // For more information, see the Fragments API guide at:
- //
- // http://developer.android.com/guide/components/fragments.html
- //
- if (savedInstanceState == null) {
- // Create the detail fragment and add it to the activity
- // using a fragment transaction.
- Bundle arguments = new Bundle();
- arguments.putInt(ProfileDetailFragment.ARG_ITEM_ID, index);
- arguments.putInt(ProfileDetailFragment.ARG_HUE, themeHue);
- mFragment = new ProfileDetailFragment();
- mFragment.setArguments(arguments);
- getSupportFragmentManager().beginTransaction()
- .add(R.id.profile_detail_container, mFragment)
- .commit();
- }
- }
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- super.onCreateOptionsMenu(menu);
- debug("profiles", "[activity] Creating profile details options menu");
- if (mFragment != null)
- mFragment.onCreateOptionsMenu(menu, getMenuInflater());
-
- return true;
- }
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- finish();
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import androidx.annotation.Nullable;
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.Locale;
@SuppressLint("Registered")
public class ProfileThemedActivity extends CrashReportingActivity {
- protected MobileLedgerProfile mProfile;
- protected void setupProfileColors() {
- final int themeHue = (mProfile == null) ? -1 : mProfile.getThemeHue();
- Colors.setupTheme(this, themeHue);
+ public static final String TAG = "prf-thm-act";
+ protected static final String PARAM_PROFILE_ID = "profile-id";
+ protected static final String PARAM_THEME = "theme";
+ protected Profile mProfile;
+ private boolean themeSetUp = false;
+ private boolean mIgnoreProfileChange;
+ private int mThemeHue;
+ protected void setupProfileColors(int newHue) {
+ if (themeSetUp && newHue == mThemeHue) {
+ Logger.debug(TAG,
+ String.format(Locale.ROOT, "Ignore request to set theme to the same value (%d)",
+ newHue));
+ return;
+ }
+
+ Logger.debug(TAG,
+ String.format(Locale.ROOT, "Changing theme from %d to %d", mThemeHue, newHue));
+
+ mThemeHue = newHue;
+ Colors.setupTheme(this, mThemeHue);
+
+ if (themeSetUp) {
+ Logger.debug(TAG,
+ "setupProfileColors(): theme already set up, supposedly the activity will be " +
+ "recreated");
+// this.recreate();
+ return;
+ }
+ themeSetUp = true;
+
+ Colors.profileThemeId = mThemeHue;
}
@Override
protected void onStart() {
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
initProfile();
- setupProfileColors();
+
+ Data.observeProfile(this, profile -> {
+ if (profile == null) {
+ Logger.debug(TAG, "No current profile, leaving");
+ return;
+ }
+
+ mProfile = profile;
+ storeProfilePref(profile);
+ int hue = profile.getTheme();
+
+ if (hue != mThemeHue) {
+ Logger.debug(TAG,
+ String.format(Locale.US, "profile observer calling setupProfileColors(%d)",
+ hue));
+ setupProfileColors(hue);
+ }
+ });
+
super.onCreate(savedInstanceState);
}
+ public void storeProfilePref(Profile profile) {
+ App.storeStartupProfileAndTheme(profile.getId(), profile.getTheme());
+ }
protected void initProfile() {
- mProfile = Data.initProfile();
+ long profileId = App.getStartupProfile();
+ int hue = App.getStartupTheme();
+ if (profileId == -1)
+ mThemeHue = Colors.DEFAULT_HUE_DEG;
+
+ Logger.debug(TAG,
+ String.format(Locale.US, "initProfile() calling setupProfileColors(%d)", hue));
+ setupProfileColors(hue);
+
+ initProfile(profileId);
+ }
+ protected void initProfile(long profileId) {
+ BaseDAO.runAsync(() -> initProfileSync(profileId));
+ }
+ private void initProfileSync(long profileId) {
+ Logger.debug(TAG, String.format(Locale.US, "Loading profile %d", profileId));
+ ProfileDAO dao = DB.get()
+ .getProfileDAO();
+ Profile profile = dao.getByIdSync(profileId);
+
+ if (profile == null) {
+ Logger.debug(TAG, String.format(Locale.ROOT, "Profile %d not found. Trying any other",
+ profileId));
+
+ profile = dao.getAnySync();
+ }
+
+ if (profile == null)
+ Logger.debug(TAG, "No profile could be loaded");
+ else
+ Logger.debug(TAG, String.format(Locale.ROOT, "Profile %d loaded. posting", profileId));
+ Data.postCurrentProfile(profile);
}
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.activity;
import android.content.Intent;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Looper;
import androidx.annotation.Nullable;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.db.DB;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
+import java.util.Locale;
public class SplashActivity extends CrashReportingActivity {
- private static final long keepActiveForMS = 500;
+ private static final long keepActiveForMS = 400;
private long startupTime;
private boolean running = true;
@Override
setContentView(R.layout.splash_activity_layout);
Logger.debug("splash", "onCreate()");
- MobileLedgerDatabase.initComplete.setValue(false);
- MobileLedgerDatabase.initComplete.observe(this, this::onDbInitDoneChanged);
+ DB.initComplete.setValue(false);
+ DB.initComplete.observe(this, this::onDbInitDoneChanged);
}
@Override
protected void onStart() {
startupTime = System.currentTimeMillis();
- AsyncTask<Void, Void, Void> dbInitTask = new DatabaseInitTask();
- dbInitTask.execute();
+ DatabaseInitThread dbInitThread = new DatabaseInitThread();
+ Logger.debug("splash", "starting dbInit task");
+ dbInitThread.start();
}
@Override
protected void onPause() {
if (now > startupTime + keepActiveForMS)
startMainActivity();
else {
- new Handler().postDelayed(this::startMainActivity,
- keepActiveForMS - (now - startupTime));
+ final long delay = keepActiveForMS - (now - startupTime);
+ Logger.debug("splash",
+ String.format(Locale.ROOT, "Scheduling main activity start in %d milliseconds",
+ delay));
+ new Handler(Looper.getMainLooper()).postDelayed(this::startMainActivity, delay);
}
}
private void startMainActivity() {
finish();
}
}
- private static class DatabaseInitTask extends AsyncTask<Void, Void, Void> {
+ private static class DatabaseInitThread extends Thread {
@Override
- protected Void doInBackground(Void... voids) {
- MobileLedgerProfile.loadAllFromDB(null);
+ public void run() {
+ long ignored = DB.get()
+ .getProfileDAO()
+ .getProfileCountSync();
- String profileUUID = MLDB.getOption(MLDB.OPT_PROFILE_UUID, null);
- MobileLedgerProfile startupProfile = Data.getProfile(profileUUID);
- if (startupProfile != null)
- Data.setCurrentProfile(startupProfile);
- return null;
- }
- @Override
- protected void onPostExecute(Void aVoid) {
- Logger.debug("splash", "DatabaseInitTask::onPostExecute()");
- super.onPostExecute(aVoid);
- MobileLedgerDatabase.initComplete.setValue(true);
+ DB.initComplete.postValue(true);
}
}
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.annotation.SuppressLint;
+import android.graphics.Typeface;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.Gravity;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.NewTransactionAccountRowBinding;
+import net.ktnx.mobileledger.db.AccountWithAmountsAutocompleteAdapter;
+import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
+import net.ktnx.mobileledger.ui.TextViewClearHelper;
+import net.ktnx.mobileledger.utils.DimensionUtils;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.text.ParseException;
+
+class NewTransactionAccountRowItemHolder extends NewTransactionItemViewHolder {
+ private final NewTransactionAccountRowBinding b;
+ private boolean ignoreFocusChanges = false;
+ private boolean inUpdate = false;
+ private boolean syncingData = false;
+ NewTransactionAccountRowItemHolder(@NonNull NewTransactionAccountRowBinding b,
+ NewTransactionItemsAdapter adapter) {
+ super(b.getRoot());
+ this.b = b;
+ new TextViewClearHelper().attachToTextView(b.comment);
+
+ b.accountRowAccName.setNextFocusForwardId(View.NO_ID);
+ b.accountRowAccAmounts.setNextFocusForwardId(View.NO_ID); // magic!
+
+ b.accountCommentButton.setOnClickListener(v -> {
+ b.comment.setVisibility(View.VISIBLE);
+ b.comment.requestFocus();
+ });
+
+ @SuppressLint("DefaultLocale") View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
+ final int id = v.getId();
+ if (hasFocus) {
+ boolean wasSyncing = syncingData;
+ syncingData = true;
+ try {
+ final int pos = getBindingAdapterPosition();
+ if (id == R.id.account_row_acc_name) {
+ adapter.noteFocusIsOnAccount(pos);
+ }
+ else if (id == R.id.account_row_acc_amounts) {
+ adapter.noteFocusIsOnAmount(pos);
+ }
+ else if (id == R.id.comment) {
+ adapter.noteFocusIsOnComment(pos);
+ }
+ else
+ throw new IllegalStateException("Where is the focus? " + id);
+ }
+ finally {
+ syncingData = wasSyncing;
+ }
+ }
+ else { // lost focus
+ if (id == R.id.account_row_acc_amounts) {
+ try {
+ String input = String.valueOf(b.accountRowAccAmounts.getText());
+ input = input.replace(Data.getDecimalSeparator(), Data.decimalDot);
+ final String newText = Data.formatNumber(Float.parseFloat(input));
+ if (!newText.equals(input)) {
+ boolean wasSyncingData = syncingData;
+ syncingData = true;
+ try {
+ // there is a listener that will propagate the change to the model
+ b.accountRowAccAmounts.setText(newText);
+ }
+ finally {
+ syncingData = wasSyncingData;
+ }
+ }
+ }
+ catch (NumberFormatException ex) {
+ // ignored
+ }
+ }
+ }
+
+ if (id == R.id.comment) {
+ commentFocusChanged(b.comment, hasFocus);
+ }
+ };
+
+ b.accountRowAccName.setOnFocusChangeListener(focusMonitor);
+ b.accountRowAccAmounts.setOnFocusChangeListener(focusMonitor);
+ b.comment.setOnFocusChangeListener(focusMonitor);
+
+ NewTransactionActivity activity = (NewTransactionActivity) b.getRoot()
+ .getContext();
+
+ b.accountRowAccName.setAdapter(new AccountWithAmountsAutocompleteAdapter(b.getRoot()
+ .getContext(),
+ mProfile));
+ b.accountRowAccName.setOnItemClickListener((parent, view, position, id) -> {
+ adapter.noteFocusIsOnAmount(position);
+ });
+
+ final TextWatcher tw = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+// debug("input", "text changed");
+ if (inUpdate)
+ return;
+
+ Logger.debug("textWatcher", "calling syncData()");
+ if (syncData()) {
+ Logger.debug("textWatcher",
+ "syncData() returned, checking if transaction is submittable");
+ adapter.model.checkTransactionSubmittable(null);
+ }
+ Logger.debug("textWatcher", "done");
+ }
+ };
+ final TextWatcher amountWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ checkAmountValid(s.toString());
+
+ if (syncData())
+ adapter.model.checkTransactionSubmittable(null);
+ }
+ };
+ b.accountRowAccName.addTextChangedListener(tw);
+ monitorComment(b.comment);
+ b.accountRowAccAmounts.addTextChangedListener(amountWatcher);
+
+ b.currencyButton.setOnClickListener(v -> {
+ CurrencySelectorFragment cpf = new CurrencySelectorFragment();
+ cpf.showPositionAndPadding();
+ cpf.setOnCurrencySelectedListener(
+ c -> adapter.setItemCurrency(getBindingAdapterPosition(), c));
+ cpf.show(activity.getSupportFragmentManager(), "currency-selector");
+ });
+
+ commentFocusChanged(b.comment, false);
+
+ adapter.model.getFocusInfo()
+ .observe(activity, this::applyFocus);
+
+ Data.currencyGap.observe(activity,
+ hasGap -> updateCurrencyPositionAndPadding(Data.currencySymbolPosition.getValue(),
+ hasGap));
+
+ Data.currencySymbolPosition.observe(activity,
+ position -> updateCurrencyPositionAndPadding(position,
+ Data.currencyGap.getValue()));
+
+ adapter.model.getShowCurrency()
+ .observe(activity, showCurrency -> {
+ if (showCurrency) {
+ b.currency.setVisibility(View.VISIBLE);
+ b.currencyButton.setVisibility(View.VISIBLE);
+ setCurrencyString(mProfile.getDefaultCommodity());
+ }
+ else {
+ b.currency.setVisibility(View.GONE);
+ b.currencyButton.setVisibility(View.GONE);
+ setCurrencyString(null);
+ }
+ });
+
+ adapter.model.getShowComments()
+ .observe(activity, show -> b.commentLayout.setVisibility(
+ show ? View.VISIBLE : View.GONE));
+ }
+ private void applyFocus(NewTransactionModel.FocusInfo focusInfo) {
+ if (ignoreFocusChanges) {
+ Logger.debug("new-trans", "Ignoring focus change");
+ return;
+ }
+ ignoreFocusChanges = true;
+ try {
+ if (((focusInfo == null) || (focusInfo.element == null) ||
+ focusInfo.position != getBindingAdapterPosition()))
+ return;
+
+ final NewTransactionModel.Item item = getItem();
+ if (item == null)
+ return;
+
+ NewTransactionModel.TransactionAccount acc = item.toTransactionAccount();
+ switch (focusInfo.element) {
+ case Amount:
+ b.accountRowAccAmounts.requestFocus();
+ break;
+ case Comment:
+ b.comment.setVisibility(View.VISIBLE);
+ b.comment.requestFocus();
+ break;
+ case Account:
+ boolean focused = b.accountRowAccName.requestFocus();
+// b.accountRowAccName.dismissDropDown();
+ if (focused)
+ Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
+ .getContext());
+ break;
+ }
+ }
+ finally {
+ ignoreFocusChanges = false;
+ }
+ }
+ public void checkAmountValid(String s) {
+ // FIXME this needs to be done in the model only
+ boolean valid = true;
+ try {
+ if (s.length() > 0) {
+ float ignored =
+ Float.parseFloat(s.replace(Data.getDecimalSeparator(), Data.decimalDot));
+ }
+ }
+ catch (NumberFormatException ex) {
+ try {
+ float ignored = Data.parseNumber(s);
+ }
+ catch (ParseException ex2) {
+ valid = false;
+ }
+ }
+
+ displayAmountValidity(valid);
+ }
+ private void displayAmountValidity(boolean valid) {
+ b.accountRowAccAmounts.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ valid ? 0 : R.drawable.ic_error_outline_black_24dp, 0, 0, 0);
+ b.accountRowAccAmounts.setMinEms(valid ? 4 : 5);
+ }
+ private void monitorComment(EditText editText) {
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+// debug("input", "text changed");
+ if (inUpdate)
+ return;
+
+ Logger.debug("textWatcher", "calling syncData()");
+ syncData();
+ Logger.debug("textWatcher",
+ "syncData() returned, checking if transaction is submittable");
+ styleComment(editText, s.toString());
+ Logger.debug("textWatcher", "done");
+ }
+ });
+ }
+ private void commentFocusChanged(TextView textView, boolean hasFocus) {
+ @ColorInt int textColor;
+ textColor = b.dummyText.getTextColors()
+ .getDefaultColor();
+ if (hasFocus) {
+ textView.setTypeface(null, Typeface.NORMAL);
+ textView.setHint(R.string.transaction_account_comment_hint);
+ }
+ else {
+ int alpha = (textColor >> 24 & 0xff);
+ alpha = 3 * alpha / 4;
+ textColor = (alpha << 24) | (0x00ffffff & textColor);
+ textView.setTypeface(null, Typeface.ITALIC);
+ textView.setHint("");
+ if (TextUtils.isEmpty(textView.getText())) {
+ textView.setVisibility(View.INVISIBLE);
+ }
+ }
+ textView.setTextColor(textColor);
+
+ }
+ private void updateCurrencyPositionAndPadding(Currency.Position position, boolean hasGap) {
+ ConstraintLayout.LayoutParams amountLP =
+ (ConstraintLayout.LayoutParams) b.accountRowAccAmounts.getLayoutParams();
+ ConstraintLayout.LayoutParams currencyLP =
+ (ConstraintLayout.LayoutParams) b.currency.getLayoutParams();
+
+ if (position == Currency.Position.before) {
+ currencyLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ currencyLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+
+ amountLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+ amountLP.endToStart = ConstraintLayout.LayoutParams.UNSET;
+ amountLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
+ amountLP.startToEnd = b.currency.getId();
+
+ b.currency.setGravity(Gravity.END);
+ }
+ else {
+ currencyLP.startToStart = ConstraintLayout.LayoutParams.UNSET;
+ currencyLP.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
+
+ amountLP.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
+ amountLP.startToEnd = ConstraintLayout.LayoutParams.UNSET;
+ amountLP.endToEnd = ConstraintLayout.LayoutParams.UNSET;
+ amountLP.endToStart = b.currency.getId();
+
+ b.currency.setGravity(Gravity.START);
+ }
+
+ amountLP.resolveLayoutDirection(b.accountRowAccAmounts.getLayoutDirection());
+ currencyLP.resolveLayoutDirection(b.currency.getLayoutDirection());
+
+ b.accountRowAccAmounts.setLayoutParams(amountLP);
+ b.currency.setLayoutParams(currencyLP);
+
+ // distance between the amount and the currency symbol
+ int gapSize = DimensionUtils.sp2px(b.currency.getContext(), 5);
+
+ if (position == Currency.Position.before) {
+ b.currency.setPaddingRelative(0, 0, hasGap ? gapSize : 0, 0);
+ }
+ else {
+ b.currency.setPaddingRelative(hasGap ? gapSize : 0, 0, 0, 0);
+ }
+ }
+ private void setCurrencyString(String currency) {
+ @ColorInt int textColor = b.dummyText.getTextColors()
+ .getDefaultColor();
+ if (TextUtils.isEmpty(currency)) {
+ b.currency.setText(R.string.currency_symbol);
+ int alpha = (textColor >> 24) & 0xff;
+ alpha = alpha * 3 / 4;
+ b.currency.setTextColor((alpha << 24) | (0x00ffffff & textColor));
+ }
+ else {
+ b.currency.setText(currency);
+ b.currency.setTextColor(textColor);
+ }
+ }
+ private void setCurrency(Currency currency) {
+ setCurrencyString((currency == null) ? null : currency.getName());
+ }
+ private void setEditable(Boolean editable) {
+ b.accountRowAccName.setEnabled(editable);
+ b.accountRowAccAmounts.setEnabled(editable);
+ }
+ private void beginUpdates() {
+ if (inUpdate)
+ throw new RuntimeException("Already in update mode");
+ inUpdate = true;
+ }
+ private void endUpdates() {
+ if (!inUpdate)
+ throw new RuntimeException("Not in update mode");
+ inUpdate = false;
+ }
+ /**
+ * syncData()
+ * <p>
+ * Stores the data from the UI elements into the model item
+ * Returns true if there were changes made that suggest transaction has to be
+ * checked for being submittable
+ */
+ private boolean syncData() {
+ if (syncingData) {
+ Logger.debug("new-trans", "skipping syncData() loop");
+ return false;
+ }
+
+ if (getBindingAdapterPosition() == RecyclerView.NO_POSITION) {
+ // probably the row was swiped out
+ Logger.debug("new-trans", "Ignoring request to syncData(): adapter position negative");
+ return false;
+ }
+
+ final NewTransactionModel.Item item = getItem();
+ if (item == null)
+ return false;
+
+ syncingData = true;
+
+ boolean significantChange = false;
+
+ try {
+ NewTransactionModel.TransactionAccount acc = item.toTransactionAccount();
+
+ // having account name is important
+ final Editable incomingAccountName = b.accountRowAccName.getText();
+ if (TextUtils.isEmpty(acc.getAccountName()) != TextUtils.isEmpty(incomingAccountName))
+ significantChange = true;
+
+ acc.setAccountName(String.valueOf(incomingAccountName));
+ final int accNameSelEnd = b.accountRowAccName.getSelectionEnd();
+ final int accNameSelStart = b.accountRowAccName.getSelectionStart();
+ acc.setAccountNameCursorPosition(accNameSelEnd);
+
+ acc.setComment(String.valueOf(b.comment.getText()));
+
+ String amount = String.valueOf(b.accountRowAccAmounts.getText());
+
+ if (acc.setAndCheckAmountText(Misc.nullIsEmpty(amount)))
+ significantChange = true;
+ displayAmountValidity(!acc.isAmountSet() || acc.isAmountValid());
+
+ final String curr = String.valueOf(b.currency.getText());
+ final String currValue;
+ if (curr.equals(b.currency.getContext()
+ .getResources()
+ .getString(R.string.currency_symbol)) || curr.isEmpty())
+ currValue = null;
+ else
+ currValue = curr;
+
+ if (!significantChange && !Misc.equalStrings(acc.getCurrency(), currValue))
+ significantChange = true;
+ acc.setCurrency(currValue);
+
+ return significantChange;
+ }
+ finally {
+ syncingData = false;
+ }
+ }
+ /**
+ * bind
+ *
+ * @param item updates the UI elements with the data from the model item
+ */
+ @SuppressLint("DefaultLocale")
+ public void bind(@NonNull NewTransactionModel.Item item) {
+ beginUpdates();
+ try {
+ syncingData = true;
+ try {
+ NewTransactionModel.TransactionAccount acc = item.toTransactionAccount();
+
+ final String incomingAccountName = acc.getAccountName();
+ final String presentAccountName = String.valueOf(b.accountRowAccName.getText());
+ if (!Misc.equalStrings(incomingAccountName, presentAccountName)) {
+ Logger.debug("bind",
+ String.format("Setting account name from '%s' to '%s' (| @ %d)",
+ presentAccountName, incomingAccountName,
+ acc.getAccountNameCursorPosition()));
+ // avoid triggering completion pop-up
+ AccountWithAmountsAutocompleteAdapter a =
+ (AccountWithAmountsAutocompleteAdapter) b.accountRowAccName.getAdapter();
+ try {
+ b.accountRowAccName.setAdapter(null);
+ b.accountRowAccName.setText(incomingAccountName);
+ b.accountRowAccName.setSelection(acc.getAccountNameCursorPosition());
+ }
+ finally {
+ b.accountRowAccName.setAdapter(a);
+ }
+ }
+
+ final String amountHint = acc.getAmountHint();
+ if (amountHint == null) {
+ b.accountRowAccAmounts.setHint(R.string.zero_amount);
+ }
+ else {
+ b.accountRowAccAmounts.setHint(amountHint);
+ }
+
+ b.accountRowAccAmounts.setImeOptions(
+ acc.isLast() ? EditorInfo.IME_ACTION_DONE : EditorInfo.IME_ACTION_NEXT);
+
+ setCurrencyString(acc.getCurrency());
+ b.accountRowAccAmounts.setText(acc.getAmountText());
+ displayAmountValidity(!acc.isAmountSet() || acc.isAmountValid());
+
+ final String comment = acc.getComment();
+ b.comment.setText(comment);
+ styleComment(b.comment, comment);
+
+ setEditable(true);
+
+ NewTransactionItemsAdapter adapter =
+ (NewTransactionItemsAdapter) getBindingAdapter();
+ if (adapter != null)
+ applyFocus(adapter.model.getFocusInfo()
+ .getValue());
+ }
+ finally {
+ syncingData = false;
+ }
+ }
+ finally {
+ endUpdates();
+ }
+ }
+ private void styleComment(EditText editText, String comment) {
+ final View focusedView = editText.findFocus();
+ editText.setTypeface(null, (focusedView == editText) ? Typeface.NORMAL : Typeface.ITALIC);
+ editText.setVisibility(
+ ((focusedView != editText) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
+ : View.VISIBLE);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.AbstractCursor;
+import android.os.Bundle;
+import android.os.ParcelFormatException;
+import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.core.view.MenuCompat;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
+
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
+import net.ktnx.mobileledger.async.GeneralBackgroundTasks;
+import net.ktnx.mobileledger.async.SendTransactionTask;
+import net.ktnx.mobileledger.async.TaskCallback;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.dao.TransactionDAO;
+import net.ktnx.mobileledger.databinding.ActivityNewTransactionBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.MatchedTemplate;
+import net.ktnx.mobileledger.ui.FabManager;
+import net.ktnx.mobileledger.ui.QR;
+import net.ktnx.mobileledger.ui.activity.ProfileThemedActivity;
+import net.ktnx.mobileledger.ui.activity.SplashActivity;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+public class NewTransactionActivity extends ProfileThemedActivity
+ implements TaskCallback, NewTransactionFragment.OnNewTransactionFragmentInteractionListener,
+ QR.QRScanTrigger, QR.QRScanResultReceiver, DescriptionSelectedCallback,
+ FabManager.FabHandler {
+ final String TAG = "new-t-a";
+ private NavController navController;
+ private NewTransactionModel model;
+ private ActivityResultLauncher<Void> qrScanLauncher;
+ private ActivityNewTransactionBinding b;
+ private FabManager fabManager;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ b = ActivityNewTransactionBinding.inflate(getLayoutInflater(), null, false);
+ setContentView(b.getRoot());
+ setSupportActionBar(b.toolbar);
+ Data.observeProfile(this, profile -> {
+ if (profile == null) {
+ Logger.debug("new-t-act", "no active profile. Redirecting to SplashActivity");
+ Intent intent = new Intent(this, SplashActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME | Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ }
+ else
+ b.toolbar.setSubtitle(profile.getName());
+ });
+
+ NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
+ getSupportFragmentManager().findFragmentById(R.id.new_transaction_nav));
+ navController = navHostFragment.getNavController();
+
+ Objects.requireNonNull(getSupportActionBar())
+ .setDisplayHomeAsUpEnabled(true);
+
+ model = new ViewModelProvider(this).get(NewTransactionModel.class);
+
+ qrScanLauncher = QR.registerLauncher(this, this);
+
+ fabManager = new FabManager(b.fabAdd);
+
+ model.isSubmittable()
+ .observe(this, isSubmittable -> {
+ if (isSubmittable) {
+ fabManager.showFab();
+ }
+ else {
+ fabManager.hideFab();
+ }
+ });
+// viewModel.checkTransactionSubmittable(listAdapter);
+
+ b.fabAdd.setOnClickListener(v -> onFabPressed());
+ }
+ @Override
+ protected void initProfile() {
+ long profileId = getIntent().getLongExtra(PARAM_PROFILE_ID, 0);
+ int profileHue = getIntent().getIntExtra(PARAM_THEME, -1);
+
+ if (profileHue < 0) {
+ Logger.debug(TAG, "Started with invalid/missing theme; quitting");
+ finish();
+ return;
+ }
+
+ if (profileId <= 0) {
+ Logger.debug(TAG, "Started with invalid/missing profile_id; quitting");
+ finish();
+ return;
+ }
+
+ setupProfileColors(profileHue);
+ initProfile(profileId);
+ }
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(R.anim.dummy, R.anim.slide_out_down);
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ public void onTransactionSave(LedgerTransaction tr) {
+ navController.navigate(R.id.action_newTransactionFragment_to_newTransactionSavingFragment);
+ try {
+
+ SendTransactionTask saver =
+ new SendTransactionTask(this, mProfile, tr, model.getSimulateSaveFlag());
+ saver.start();
+ }
+ catch (Exception e) {
+ debug("new-transaction", "Unknown error: " + e);
+
+ Bundle b = new Bundle();
+ b.putString("error", "unknown error");
+ navController.navigate(R.id.newTransactionFragment, b);
+ }
+ }
+ public boolean onSimulateCrashMenuItemClicked(MenuItem item) {
+ debug("crash", "Will crash intentionally");
+ GeneralBackgroundTasks.run(() -> { throw new RuntimeException("Simulated crash");});
+ return true;
+ }
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ if (!BuildConfig.DEBUG)
+ return true;
+
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.new_transaction, menu);
+
+ MenuCompat.setGroupDividerEnabled(menu, true);
+
+ menu.findItem(R.id.action_simulate_save)
+ .setOnMenuItemClickListener(this::onToggleSimulateSaveMenuItemClicked);
+ menu.findItem(R.id.action_simulate_crash)
+ .setOnMenuItemClickListener(this::onSimulateCrashMenuItemClicked);
+
+ model.getSimulateSave()
+ .observe(this, state -> {
+ menu.findItem(R.id.action_simulate_save)
+ .setChecked(state);
+ b.simulationLabel.setVisibility(state ? View.VISIBLE : View.GONE);
+ });
+
+ return true;
+ }
+
+
+ public int dp2px(float dp) {
+ return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
+ getResources().getDisplayMetrics()));
+ }
+ @Override
+ public void onTransactionSaveDone(String error, Object arg) {
+ Bundle b = new Bundle();
+ if (error != null) {
+ b.putString("error", error);
+ navController.navigate(R.id.action_newTransactionSavingFragment_Failure, b);
+ }
+ else {
+ navController.navigate(R.id.action_newTransactionSavingFragment_Success, b);
+
+ BaseDAO.runAsync(() -> commitToDb((LedgerTransaction) arg));
+ }
+ }
+ public void commitToDb(LedgerTransaction tr) {
+ TransactionWithAccounts dbTransaction = tr.toDBO();
+ DB.get()
+ .getTransactionDAO()
+ .appendSync(dbTransaction);
+ }
+ public boolean onToggleSimulateSaveMenuItemClicked(MenuItem item) {
+ model.toggleSimulateSave();
+ return true;
+ }
+
+ @Override
+ public void triggerQRScan() {
+ qrScanLauncher.launch(null);
+ }
+ private void startNewPatternActivity(String scanned) {
+ Intent intent = new Intent(this, TemplatesActivity.class);
+ Bundle args = new Bundle();
+ args.putString(TemplatesActivity.ARG_ADD_TEMPLATE, scanned);
+ startActivity(intent, args);
+ }
+ private void alertNoTemplateMatch(String scanned) {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+ builder.setCancelable(true)
+ .setMessage(R.string.no_template_matches)
+ .setPositiveButton(R.string.add_button,
+ (dialog, which) -> startNewPatternActivity(scanned))
+ .create()
+ .show();
+ }
+ public void onQRScanResult(String text) {
+ Logger.debug("qr", String.format("Got QR scan result [%s]", text));
+
+ if (Misc.emptyIsNull(text) == null)
+ return;
+
+ LiveData<List<TemplateHeader>> allTemplates = DB.get()
+ .getTemplateDAO()
+ .getTemplates();
+ allTemplates.observe(this, templateHeaders -> {
+ ArrayList<MatchedTemplate> matchingFallbackTemplates = new ArrayList<>();
+ ArrayList<MatchedTemplate> matchingTemplates = new ArrayList<>();
+
+ for (TemplateHeader ph : templateHeaders) {
+ String patternSource = ph.getRegularExpression();
+ if (Misc.emptyIsNull(patternSource) == null)
+ continue;
+ try {
+ Pattern pattern = Pattern.compile(patternSource);
+ Matcher matcher = pattern.matcher(text);
+ if (!matcher.matches())
+ continue;
+
+ Logger.debug("pattern",
+ String.format("Pattern '%s' [%s] matches '%s'", ph.getName(),
+ patternSource, text));
+ if (ph.isFallback())
+ matchingFallbackTemplates.add(
+ new MatchedTemplate(ph, matcher.toMatchResult()));
+ else
+ matchingTemplates.add(new MatchedTemplate(ph, matcher.toMatchResult()));
+ }
+ catch (ParcelFormatException e) {
+ // ignored
+ Logger.debug("pattern",
+ String.format("Error compiling regular expression '%s'", patternSource),
+ e);
+ }
+ }
+
+ if (matchingTemplates.isEmpty())
+ matchingTemplates = matchingFallbackTemplates;
+
+ if (matchingTemplates.isEmpty())
+ alertNoTemplateMatch(text);
+ else if (matchingTemplates.size() == 1)
+ model.applyTemplate(matchingTemplates.get(0), text);
+ else
+ chooseTemplate(matchingTemplates, text);
+ });
+ }
+ private void chooseTemplate(ArrayList<MatchedTemplate> matchingTemplates, String matchedText) {
+ final String templateNameColumn = "name";
+ AbstractCursor cursor = new AbstractCursor() {
+ @Override
+ public int getCount() {
+ return matchingTemplates.size();
+ }
+ @Override
+ public String[] getColumnNames() {
+ return new String[]{"_id", templateNameColumn};
+ }
+ @Override
+ public String getString(int column) {
+ if (column == 0)
+ return String.valueOf(getPosition());
+ return matchingTemplates.get(getPosition()).templateHead.getName();
+ }
+ @Override
+ public short getShort(int column) {
+ if (column == 0)
+ return (short) getPosition();
+ return -1;
+ }
+ @Override
+ public int getInt(int column) {
+ return getShort(column);
+ }
+ @Override
+ public long getLong(int column) {
+ return getShort(column);
+ }
+ @Override
+ public float getFloat(int column) {
+ return getShort(column);
+ }
+ @Override
+ public double getDouble(int column) {
+ return getShort(column);
+ }
+ @Override
+ public boolean isNull(int column) {
+ return false;
+ }
+ @Override
+ public int getColumnCount() {
+ return 2;
+ }
+ };
+
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+ builder.setCancelable(true)
+ .setTitle(R.string.choose_template_to_apply)
+ .setIcon(R.drawable.ic_baseline_auto_graph_24)
+ .setSingleChoiceItems(cursor, -1, templateNameColumn, (dialog, which) -> {
+ model.applyTemplate(matchingTemplates.get(which), matchedText);
+ dialog.dismiss();
+ })
+ .create()
+ .show();
+ }
+ public void onDescriptionSelected(String description) {
+ debug("description selected", description);
+ if (!model.accountListIsEmpty())
+ return;
+
+ BaseDAO.runAsync(() -> {
+ String accFilter = mProfile.getPreferredAccountsFilter();
+
+ TransactionDAO trDao = DB.get()
+ .getTransactionDAO();
+
+ TransactionWithAccounts tr = null;
+
+ if (Misc.emptyIsNull(accFilter) != null)
+ tr = trDao.getFirstByDescriptionHavingAccountSync(description, accFilter);
+ if (tr == null)
+ tr = trDao.getFirstByDescriptionSync(description);
+
+ if (tr != null)
+ model.loadTransactionIntoModel(tr);
+ });
+ }
+ private void onFabPressed() {
+ fabManager.hideFab();
+ Misc.hideSoftKeyboard(this);
+
+ LedgerTransaction tr = model.constructLedgerTransaction();
+
+ onTransactionSave(tr);
+ }
+ @Override
+ public Context getContext() {
+ return this;
+ }
+ @Override
+ public void showManagedFab() {
+ if (Objects.requireNonNull(model.isSubmittable()
+ .getValue()))
+ fabManager.showFab();
+ }
+ @Override
+ public void hideManagedFab() {
+ fabManager.hideFab();
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.ui.FabManager;
+import net.ktnx.mobileledger.ui.QR;
+import net.ktnx.mobileledger.ui.profiles.ProfileDetailActivity;
+import net.ktnx.mobileledger.utils.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Activities that contain this fragment must implement the
+ * {@link OnNewTransactionFragmentInteractionListener} interface
+ * to handle interaction events.
+ */
+
+// TODO: offer to undo account remove-on-swipe
+
+public class NewTransactionFragment extends Fragment {
+ private NewTransactionItemsAdapter listAdapter;
+ private NewTransactionModel viewModel;
+ private OnNewTransactionFragmentInteractionListener mListener;
+ private Profile mProfile;
+ public NewTransactionFragment() {
+ // Required empty public constructor
+ setHasOptionsMenu(true);
+ }
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ final FragmentActivity activity = getActivity();
+
+ inflater.inflate(R.menu.new_transaction_fragment, menu);
+
+ menu.findItem(R.id.scan_qr)
+ .setOnMenuItemClickListener(this::onScanQrAction);
+
+ menu.findItem(R.id.action_reset_new_transaction_activity)
+ .setOnMenuItemClickListener(item -> {
+ viewModel.reset();
+ return true;
+ });
+
+ final MenuItem toggleCurrencyItem = menu.findItem(R.id.toggle_currency);
+ toggleCurrencyItem.setOnMenuItemClickListener(item -> {
+ viewModel.toggleCurrencyVisible();
+ return true;
+ });
+ if (activity != null)
+ viewModel.getShowCurrency()
+ .observe(activity, toggleCurrencyItem::setChecked);
+
+ final MenuItem toggleCommentsItem = menu.findItem(R.id.toggle_comments);
+ toggleCommentsItem.setOnMenuItemClickListener(item -> {
+ viewModel.toggleShowComments();
+ return true;
+ });
+ if (activity != null)
+ viewModel.getShowComments()
+ .observe(activity, toggleCommentsItem::setChecked);
+ }
+ private boolean onScanQrAction(MenuItem item) {
+ try {
+ Context ctx = requireContext();
+ if (ctx instanceof QR.QRScanTrigger)
+ ((QR.QRScanTrigger) ctx).triggerQRScan();
+ }
+ catch (Exception e) {
+ Logger.debug("qr", "Error launching QR scanner", e);
+ }
+
+ return true;
+ }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ // Inflate the layout for this fragment
+ return inflater.inflate(R.layout.fragment_new_transaction, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ FragmentActivity activity = getActivity();
+ if (activity == null)
+ throw new IllegalStateException(
+ "getActivity() returned null within onActivityCreated()");
+
+ viewModel = new ViewModelProvider(activity).get(NewTransactionModel.class);
+ viewModel.observeDataProfile(this);
+ mProfile = Data.getProfile();
+ listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
+
+ viewModel.getItems()
+ .observe(getViewLifecycleOwner(), newList -> listAdapter.setItems(newList));
+
+ RecyclerView list = activity.findViewById(R.id.new_transaction_accounts);
+ list.setAdapter(listAdapter);
+ list.setLayoutManager(new LinearLayoutManager(activity));
+
+ Data.observeProfile(getViewLifecycleOwner(), profile -> {
+ mProfile = profile;
+ listAdapter.setProfile(profile);
+ });
+ boolean keep = false;
+
+ Bundle args = getArguments();
+ if (args != null) {
+ String error = args.getString("error");
+ if (error != null) {
+ Logger.debug("new-trans-f", String.format("Got error: %s", error));
+
+ Context context = getContext();
+ if (context != null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ final Resources resources = context.getResources();
+ final StringBuilder message = new StringBuilder();
+ message.append(resources.getString(R.string.err_json_send_error_head))
+ .append("\n\n")
+ .append(error)
+ .append("\n\n");
+ if (API.valueOf(mProfile.getApiVersion())
+ .equals(API.auto))
+ message.append(
+ resources.getString(R.string.err_json_send_error_unsupported));
+ else {
+ message.append(resources.getString(R.string.err_json_send_error_tail));
+ builder.setPositiveButton(R.string.btn_profile_options, (dialog, which) -> {
+ Logger.debug("error", "will start profile editor");
+ ProfileDetailActivity.start(context, mProfile);
+ });
+ }
+ builder.setMessage(message);
+ builder.create()
+ .show();
+ }
+ else {
+ Snackbar.make(list, error, Snackbar.LENGTH_INDEFINITE)
+ .show();
+ }
+ keep = true;
+ }
+ }
+
+ int focused = 0;
+ FocusedElement element = null;
+ if (savedInstanceState != null) {
+ keep |= savedInstanceState.getBoolean("keep", true);
+ focused = savedInstanceState.getInt("focused-item", 0);
+ final String focusedElementString = savedInstanceState.getString("focused-element");
+ if (focusedElementString != null)
+ element = FocusedElement.valueOf(focusedElementString);
+ }
+
+ if (!keep) {
+ // we need the DB up and running
+ Data.observeProfile(getViewLifecycleOwner(), p -> {
+ if (p != null)
+ viewModel.reset();
+ });
+ }
+ else {
+ viewModel.noteFocusChanged(focused, element);
+ }
+
+ ProgressBar p = activity.findViewById(R.id.progressBar);
+ viewModel.getBusyFlag()
+ .observe(getViewLifecycleOwner(), isBusy -> {
+ if (isBusy) {
+// Handler h = new Handler();
+// h.postDelayed(() -> {
+// if (viewModel.getBusyFlag())
+// p.setVisibility(View.VISIBLE);
+//
+// }, 10);
+ p.setVisibility(View.VISIBLE);
+ }
+ else
+ p.setVisibility(View.INVISIBLE);
+ });
+
+ if (activity instanceof FabManager.FabHandler)
+ FabManager.handle((FabManager.FabHandler) activity, list);
+ }
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean("keep", true);
+ final NewTransactionModel.FocusInfo focusInfo = viewModel.getFocusInfo()
+ .getValue();
+ if (focusInfo != null) {
+ final int focusedItem = focusInfo.position;
+ if (focusedItem >= 0)
+ outState.putInt("focused-item", focusedItem);
+ if (focusInfo.element != null)
+ outState.putString("focused-element", focusInfo.element.toString());
+ }
+ }
+
+ @Override
+ public void onAttach(@NotNull Context context) {
+ super.onAttach(context);
+ if (context instanceof OnNewTransactionFragmentInteractionListener) {
+ mListener = (OnNewTransactionFragmentInteractionListener) context;
+ }
+ else {
+ throw new RuntimeException(
+ context.toString() + " must implement OnFragmentInteractionListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mListener = null;
+ }
+
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ * <p>
+ * See the Android Training lesson <a href=
+ * "http://developer.android.com/training/basics/fragments/communicating.html"
+ * >Communicating with Other Fragments</a> for more information.
+ */
+ public interface OnNewTransactionFragmentInteractionListener {
+ void onTransactionSave(LedgerTransaction tr);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.annotation.SuppressLint;
+import android.graphics.Typeface;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding;
+import net.ktnx.mobileledger.db.TransactionDescriptionAutocompleteAdapter;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.FutureDates;
+import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+
+class NewTransactionHeaderItemHolder extends NewTransactionItemViewHolder
+ implements DatePickerFragment.DatePickedListener {
+ private final NewTransactionHeaderRowBinding b;
+ private boolean ignoreFocusChanges = false;
+ private String decimalSeparator;
+ private boolean inUpdate = false;
+ private boolean syncingData = false;
+ NewTransactionHeaderItemHolder(@NonNull NewTransactionHeaderRowBinding b,
+ NewTransactionItemsAdapter adapter) {
+ super(b.getRoot());
+ this.b = b;
+
+ b.newTransactionDescription.setNextFocusForwardId(View.NO_ID);
+
+ b.newTransactionDate.setOnClickListener(v -> pickTransactionDate());
+
+ b.transactionCommentButton.setOnClickListener(v -> {
+ b.transactionComment.setVisibility(View.VISIBLE);
+ b.transactionComment.requestFocus();
+ });
+
+ @SuppressLint("DefaultLocale") View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
+ final int id = v.getId();
+ if (hasFocus) {
+ boolean wasSyncing = syncingData;
+ syncingData = true;
+ try {
+ final int pos = getBindingAdapterPosition();
+ if (id == R.id.transaction_comment) {
+ adapter.noteFocusIsOnTransactionComment(pos);
+ }
+ else if (id == R.id.new_transaction_description) {
+ adapter.noteFocusIsOnDescription(pos);
+ }
+ else
+ throw new IllegalStateException("Where is the focus? " + id);
+ }
+ finally {
+ syncingData = wasSyncing;
+ }
+ }
+
+ if (id == R.id.transaction_comment) {
+ commentFocusChanged(b.transactionComment, hasFocus);
+ }
+ };
+
+ b.newTransactionDescription.setOnFocusChangeListener(focusMonitor);
+ b.transactionComment.setOnFocusChangeListener(focusMonitor);
+
+ NewTransactionActivity activity = (NewTransactionActivity) b.getRoot()
+ .getContext();
+
+ b.newTransactionDescription.setAdapter(
+ new TransactionDescriptionAutocompleteAdapter(activity));
+ b.newTransactionDescription.setOnItemClickListener(
+ (parent, view, position, id) -> activity.onDescriptionSelected(
+ parent.getItemAtPosition(position)
+ .toString()));
+
+ decimalSeparator = "";
+ Data.locale.observe(activity, locale -> decimalSeparator = String.valueOf(
+ DecimalFormatSymbols.getInstance(locale)
+ .getMonetaryDecimalSeparator()));
+
+ final TextWatcher tw = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+// debug("input", "text changed");
+ if (inUpdate)
+ return;
+
+ Logger.debug("textWatcher", "calling syncData()");
+ if (syncData()) {
+ Logger.debug("textWatcher",
+ "syncData() returned, checking if transaction is submittable");
+ adapter.model.checkTransactionSubmittable(null);
+ }
+ Logger.debug("textWatcher", "done");
+ }
+ };
+ b.newTransactionDescription.addTextChangedListener(tw);
+ monitorComment(b.transactionComment);
+
+ commentFocusChanged(b.transactionComment, false);
+
+ adapter.model.getFocusInfo()
+ .observe(activity, this::applyFocus);
+
+ adapter.model.getShowComments()
+ .observe(activity, show -> b.transactionCommentLayout.setVisibility(
+ show ? View.VISIBLE : View.GONE));
+ }
+ private void applyFocus(NewTransactionModel.FocusInfo focusInfo) {
+ if (ignoreFocusChanges) {
+ Logger.debug("new-trans", "Ignoring focus change");
+ return;
+ }
+ ignoreFocusChanges = true;
+ try {
+ if (((focusInfo == null) || (focusInfo.element == null) ||
+ focusInfo.position != getBindingAdapterPosition()))
+ return;
+
+ final NewTransactionModel.Item item = getItem();
+ if (item == null)
+ return;
+
+ NewTransactionModel.Item head = item.toTransactionHead();
+ // bad idea - double pop-up, and not really necessary.
+ // the user can tap the input to get the calendar
+ //if (!tvDate.hasFocus()) tvDate.requestFocus();
+ switch (focusInfo.element) {
+ case TransactionComment:
+ b.transactionComment.setVisibility(View.VISIBLE);
+ b.transactionComment.requestFocus();
+ break;
+ case Description:
+ boolean focused = b.newTransactionDescription.requestFocus();
+// tvDescription.dismissDropDown();
+ if (focused)
+ Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
+ .getContext());
+ break;
+ }
+ }
+ finally {
+ ignoreFocusChanges = false;
+ }
+ }
+ private void monitorComment(EditText editText) {
+ editText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+// debug("input", "text changed");
+ if (inUpdate)
+ return;
+
+ Logger.debug("textWatcher", "calling syncData()");
+ syncData();
+ Logger.debug("textWatcher",
+ "syncData() returned, checking if transaction is submittable");
+ styleComment(editText, s.toString());
+ Logger.debug("textWatcher", "done");
+ }
+ });
+ }
+ private void commentFocusChanged(TextView textView, boolean hasFocus) {
+ @ColorInt int textColor;
+ textColor = b.dummyText.getTextColors()
+ .getDefaultColor();
+ if (hasFocus) {
+ textView.setTypeface(null, Typeface.NORMAL);
+ textView.setHint(R.string.transaction_account_comment_hint);
+ }
+ else {
+ int alpha = (textColor >> 24 & 0xff);
+ alpha = 3 * alpha / 4;
+ textColor = (alpha << 24) | (0x00ffffff & textColor);
+ textView.setTypeface(null, Typeface.ITALIC);
+ textView.setHint("");
+ if (TextUtils.isEmpty(textView.getText())) {
+ textView.setVisibility(View.INVISIBLE);
+ }
+ }
+ textView.setTextColor(textColor);
+
+ }
+ private void setEditable(Boolean editable) {
+ b.newTransactionDate.setEnabled(editable);
+ b.newTransactionDescription.setEnabled(editable);
+ }
+ private void beginUpdates() {
+ if (inUpdate)
+ throw new RuntimeException("Already in update mode");
+ inUpdate = true;
+ }
+ private void endUpdates() {
+ if (!inUpdate)
+ throw new RuntimeException("Not in update mode");
+ inUpdate = false;
+ }
+ /**
+ * syncData()
+ * <p>
+ * Stores the data from the UI elements into the model item
+ * Returns true if there were changes made that suggest transaction has to be
+ * checked for being submittable
+ */
+ private boolean syncData() {
+ if (syncingData) {
+ Logger.debug("new-trans", "skipping syncData() loop");
+ return false;
+ }
+
+ if (getBindingAdapterPosition() == RecyclerView.NO_POSITION) {
+ // probably the row was swiped out
+ Logger.debug("new-trans", "Ignoring request to syncData(): adapter position negative");
+ return false;
+ }
+
+
+ boolean significantChange = false;
+
+ syncingData = true;
+ try {
+ final NewTransactionModel.Item item = getItem();
+ if (item == null)
+ return false;
+
+ NewTransactionModel.TransactionHead head = item.toTransactionHead();
+
+ head.setDate(String.valueOf(b.newTransactionDate.getText()));
+
+ // transaction description is required
+ if (TextUtils.isEmpty(head.getDescription()) !=
+ TextUtils.isEmpty(b.newTransactionDescription.getText()))
+ significantChange = true;
+
+ head.setDescription(String.valueOf(b.newTransactionDescription.getText()));
+ head.setComment(String.valueOf(b.transactionComment.getText()));
+
+ return significantChange;
+ }
+ catch (ParseException e) {
+ throw new RuntimeException("Should not happen", e);
+ }
+ finally {
+ syncingData = false;
+ }
+ }
+ private void pickTransactionDate() {
+ DatePickerFragment picker = new DatePickerFragment();
+ picker.setFutureDates(FutureDates.valueOf(mProfile.getFutureDates()));
+ picker.setOnDatePickedListener(this);
+ picker.setCurrentDateFromText(b.newTransactionDate.getText());
+ picker.show(((NewTransactionActivity) b.getRoot()
+ .getContext()).getSupportFragmentManager(), null);
+ }
+ /**
+ * bind
+ *
+ * @param item updates the UI elements with the data from the model item
+ */
+ @SuppressLint("DefaultLocale")
+ public void bind(@NonNull NewTransactionModel.Item item) {
+ beginUpdates();
+ try {
+ syncingData = true;
+ try {
+ NewTransactionModel.TransactionHead head = item.toTransactionHead();
+ b.newTransactionDate.setText(head.getFormattedDate());
+
+ // avoid triggering completion pop-up
+ ListAdapter a = b.newTransactionDescription.getAdapter();
+ try {
+ b.newTransactionDescription.setAdapter(null);
+ b.newTransactionDescription.setText(head.getDescription());
+ }
+ finally {
+ b.newTransactionDescription.setAdapter(
+ (TransactionDescriptionAutocompleteAdapter) a);
+ }
+
+ final String comment = head.getComment();
+ b.transactionComment.setText(comment);
+ styleComment(b.transactionComment, comment); // would hide or make it visible
+
+ setEditable(true);
+
+ NewTransactionItemsAdapter adapter =
+ (NewTransactionItemsAdapter) getBindingAdapter();
+ if (adapter != null)
+ applyFocus(adapter.model.getFocusInfo()
+ .getValue());
+ }
+ finally {
+ syncingData = false;
+ }
+ }
+ finally {
+ endUpdates();
+ }
+ }
+ private void styleComment(EditText editText, String comment) {
+ final View focusedView = editText.findFocus();
+ editText.setTypeface(null, (focusedView == editText) ? Typeface.NORMAL : Typeface.ITALIC);
+ editText.setVisibility(
+ ((focusedView != editText) && TextUtils.isEmpty(comment)) ? View.INVISIBLE
+ : View.VISIBLE);
+ }
+ @Override
+ public void onDatePicked(int year, int month, int day) {
+ final NewTransactionModel.Item item = getItem();
+ if (item == null)
+ return;
+
+ final NewTransactionModel.TransactionHead head = item.toTransactionHead();
+ head.setDate(new SimpleDate(year, month + 1, day));
+ b.newTransactionDate.setText(head.getFormattedDate());
+
+ boolean focused = b.newTransactionDescription.requestFocus();
+ if (focused)
+ Misc.showSoftKeyboard((NewTransactionActivity) b.getRoot()
+ .getContext());
+
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.model.Data;
+
+abstract class NewTransactionItemViewHolder extends RecyclerView.ViewHolder {
+ final Profile mProfile;
+ public NewTransactionItemViewHolder(@NonNull View itemView) {
+ super(itemView);
+ mProfile = Data.getProfile();
+ }
+ @Nullable
+ NewTransactionModel.Item getItem() {
+ NewTransactionItemsAdapter adapter = (NewTransactionItemsAdapter) getBindingAdapter();
+ if (adapter == null)
+ return null;
+ return adapter.getItem(getBindingAdapterPosition());
+ }
+ abstract public void bind(NewTransactionModel.Item item);
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.NewTransactionAccountRowBinding;
+import net.ktnx.mobileledger.databinding.NewTransactionHeaderRowBinding;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemViewHolder> {
+ private static final int ITEM_VIEW_TYPE_HEADER = 1;
+ private static final int ITEM_VIEW_TYPE_ACCOUNT = 2;
+ final NewTransactionModel model;
+ private final ItemTouchHelper touchHelper;
+ private final AsyncListDiffer<NewTransactionModel.Item> differ =
+ new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<NewTransactionModel.Item>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull NewTransactionModel.Item oldItem,
+ @NonNull NewTransactionModel.Item newItem) {
+// Logger.debug("new-trans",
+// String.format("comparing ids of {%s} and {%s}", oldItem.toString(),
+// newItem.toString()));
+ return oldItem.getId() == newItem.getId();
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull NewTransactionModel.Item oldItem,
+ @NonNull NewTransactionModel.Item newItem) {
+
+// Logger.debug("new-trans",
+// String.format("comparing contents of {%s} and {%s}", oldItem
+// .toString(),
+// newItem.toString()));
+ return oldItem.equalContents(newItem);
+ }
+ });
+ private Profile mProfile;
+ private int checkHoldCounter = 0;
+ NewTransactionItemsAdapter(NewTransactionModel viewModel, Profile profile) {
+ super();
+ setHasStableIds(true);
+ model = viewModel;
+ mProfile = profile;
+
+
+ NewTransactionItemsAdapter adapter = this;
+
+ touchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return true;
+ }
+ @Override
+ public boolean canDropOver(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder current,
+ @NonNull RecyclerView.ViewHolder target) {
+ final int adapterPosition = target.getBindingAdapterPosition();
+
+ // first item is immovable
+ if (adapterPosition == 0)
+ return false;
+
+ return super.canDropOver(recyclerView, current, target);
+ }
+ @Override
+ public int getMovementFlags(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder) {
+ int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
+ // the top (date and description) and the bottom (padding) items are always there
+ final int adapterPosition = viewHolder.getBindingAdapterPosition();
+ if (adapterPosition > 0) {
+ flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
+ ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
+ makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
+ ItemTouchHelper.START | ItemTouchHelper.END);
+ }
+
+ return flags;
+ }
+ @Override
+ public boolean onMove(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder,
+ @NonNull RecyclerView.ViewHolder target) {
+
+ model.moveItem(viewHolder.getBindingAdapterPosition(),
+ target.getBindingAdapterPosition());
+ return true;
+ }
+ @Override
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+ int pos = viewHolder.getBindingAdapterPosition();
+ viewModel.removeItem(pos);
+ }
+ });
+ }
+ @Override
+ public int getItemViewType(int position) {
+ final ItemType type = differ.getCurrentList()
+ .get(position)
+ .getType();
+ switch (type) {
+ case generalData:
+ return ITEM_VIEW_TYPE_HEADER;
+ case transactionRow:
+ return ITEM_VIEW_TYPE_ACCOUNT;
+ default:
+ throw new RuntimeException("Can't handle " + type);
+ }
+ }
+ @Override
+ public long getItemId(int position) {
+ return differ.getCurrentList()
+ .get(position)
+ .getId();
+ }
+ public void setProfile(Profile profile) {
+ mProfile = profile;
+ }
+ @NonNull
+ @Override
+ public NewTransactionItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ switch (viewType) {
+ case ITEM_VIEW_TYPE_HEADER:
+ NewTransactionHeaderRowBinding headerBinding =
+ NewTransactionHeaderRowBinding.inflate(
+ LayoutInflater.from(parent.getContext()), parent, false);
+ final NewTransactionHeaderItemHolder headerHolder =
+ new NewTransactionHeaderItemHolder(headerBinding, this);
+ Logger.debug("new-trans", "Creating new Header ViewHolder " +
+ Integer.toHexString(headerHolder.hashCode()));
+ return headerHolder;
+ case ITEM_VIEW_TYPE_ACCOUNT:
+ NewTransactionAccountRowBinding accBinding =
+ NewTransactionAccountRowBinding.inflate(
+ LayoutInflater.from(parent.getContext()), parent, false);
+ final NewTransactionAccountRowItemHolder accHolder =
+ new NewTransactionAccountRowItemHolder(accBinding, this);
+ Logger.debug("new-trans", "Creating new AccountRow ViewHolder " +
+ Integer.toHexString(accHolder.hashCode()));
+ return accHolder;
+ default:
+ throw new RuntimeException("Cant handle view type " + viewType);
+ }
+ }
+ @Override
+ public void onBindViewHolder(@NonNull NewTransactionItemViewHolder holder, int position) {
+ Logger.debug("bind",
+ String.format(Locale.US, "Binding item at position %d, holder %s", position,
+ Integer.toHexString(holder.hashCode())));
+ NewTransactionModel.Item item = Objects.requireNonNull(differ.getCurrentList()
+ .get(position));
+ holder.bind(item);
+ Logger.debug("bind", String.format(Locale.US, "Bound %s item at position %d", item.getType()
+ .toString(),
+ position));
+ }
+ @Override
+ public int getItemCount() {
+ return differ.getCurrentList()
+ .size();
+ }
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+ touchHelper.attachToRecyclerView(recyclerView);
+ }
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ touchHelper.attachToRecyclerView(null);
+ super.onDetachedFromRecyclerView(recyclerView);
+ }
+ void noteFocusIsOnAccount(int position) {
+ model.noteFocusChanged(position, FocusedElement.Account);
+ }
+ void noteFocusIsOnAmount(int position) {
+ model.noteFocusChanged(position, FocusedElement.Amount);
+ }
+ void noteFocusIsOnComment(int position) {
+ model.noteFocusChanged(position, FocusedElement.Comment);
+ }
+ void noteFocusIsOnTransactionComment(int position) {
+ model.noteFocusChanged(position, FocusedElement.TransactionComment);
+ }
+ public void noteFocusIsOnDescription(int pos) {
+ model.noteFocusChanged(pos, FocusedElement.Description);
+ }
+ private void holdSubmittableChecks() {
+ checkHoldCounter++;
+ }
+ private void releaseSubmittableChecks() {
+ if (checkHoldCounter == 0)
+ throw new RuntimeException("Asymmetrical call to releaseSubmittableChecks");
+ checkHoldCounter--;
+ }
+ void setItemCurrency(int position, String newCurrency) {
+ model.setItemCurrency(position, newCurrency);
+ }
+
+ public void setItems(List<NewTransactionModel.Item> newList) {
+ Logger.debug("new-trans", "adapter: submitting new item list");
+ differ.submitList(newList);
+ }
+ public NewTransactionModel.Item getItem(int position) {
+ return differ.getCurrentList()
+ .get(position);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.new_transaction;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.db.Currency;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TransactionWithAccounts;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.InertMutableLiveData;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.model.MatchedTemplate;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.MatchResult;
+
+enum ItemType {generalData, transactionRow}
+
+enum FocusedElement {Account, Comment, Amount, Description, TransactionComment}
+
+
+public class NewTransactionModel extends ViewModel {
+ private static final int MIN_ITEMS = 3;
+ private final MutableLiveData<Boolean> showCurrency = new MutableLiveData<>(false);
+ private final MutableLiveData<Boolean> isSubmittable = new InertMutableLiveData<>(false);
+ private final MutableLiveData<Boolean> showComments = new MutableLiveData<>(true);
+ private final MutableLiveData<List<Item>> items = new MutableLiveData<>();
+ private final MutableLiveData<Boolean> simulateSave = new InertMutableLiveData<>(false);
+ private final AtomicInteger busyCounter = new AtomicInteger(0);
+ private final MutableLiveData<Boolean> busyFlag = new InertMutableLiveData<>(false);
+ private final Observer<Profile> profileObserver = profile -> {
+ if (profile != null) {
+ showCurrency.postValue(profile.getShowCommodityByDefault());
+ showComments.postValue(profile.getShowCommentsByDefault());
+ }
+ };
+ private final MutableLiveData<FocusInfo> focusInfo = new MutableLiveData<>();
+ private boolean observingDataProfile;
+ public NewTransactionModel() {
+ }
+ public LiveData<Boolean> getShowCurrency() {
+ return showCurrency;
+ }
+ public LiveData<List<Item>> getItems() {
+ return items;
+ }
+ private void setItems(@NonNull List<Item> newList) {
+ checkTransactionSubmittable(newList);
+ setItemsWithoutSubmittableChecks(newList);
+ }
+ private void replaceItems(@NonNull List<Item> newList) {
+ renumberItems();
+
+ setItems(newList);
+ }
+ /**
+ * make old items replaceable in-place. makes the new values visually blend in
+ */
+ private void renumberItems() {
+ renumberItems(items.getValue());
+ }
+ private void renumberItems(List<Item> list) {
+ if (list == null) {
+ return;
+ }
+
+ int id = 0;
+ for (Item item : list)
+ item.id = id++;
+ }
+ private void setItemsWithoutSubmittableChecks(@NonNull List<Item> list) {
+ final int cnt = list.size();
+ for (int i = 1; i < cnt - 1; i++) {
+ final TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+ if (item.isLast) {
+ TransactionAccount replacement = new TransactionAccount(item);
+ replacement.isLast = false;
+ list.set(i, replacement);
+ }
+ }
+ final TransactionAccount last = list.get(cnt - 1)
+ .toTransactionAccount();
+ if (!last.isLast) {
+ TransactionAccount replacement = new TransactionAccount(last);
+ replacement.isLast = true;
+ list.set(cnt - 1, replacement);
+ }
+
+ if (BuildConfig.DEBUG)
+ dumpItemList("Before setValue()", list);
+ items.setValue(list);
+ }
+ private List<Item> copyList() {
+ List<Item> copy = new ArrayList<>();
+ List<Item> oldList = items.getValue();
+
+ if (oldList != null)
+ for (Item item : oldList) {
+ copy.add(Item.from(item));
+ }
+
+ return copy;
+ }
+ private List<Item> copyListWithoutItem(int position) {
+ List<Item> copy = new ArrayList<>();
+ List<Item> oldList = items.getValue();
+
+ if (oldList != null) {
+ int i = 0;
+ for (Item item : oldList) {
+ if (i++ == position)
+ continue;
+ copy.add(Item.from(item));
+ }
+ }
+
+ return copy;
+ }
+ private List<Item> shallowCopyList() {
+ return new ArrayList<>(Objects.requireNonNull(items.getValue()));
+ }
+ LiveData<Boolean> getShowComments() {
+ return showComments;
+ }
+ void observeDataProfile(LifecycleOwner activity) {
+ if (!observingDataProfile)
+ Data.observeProfile(activity, profileObserver);
+ observingDataProfile = true;
+ }
+ boolean getSimulateSaveFlag() {
+ Boolean value = simulateSave.getValue();
+ if (value == null)
+ return false;
+ return value;
+ }
+ LiveData<Boolean> getSimulateSave() {
+ return simulateSave;
+ }
+ void toggleSimulateSave() {
+ simulateSave.setValue(!getSimulateSaveFlag());
+ }
+ LiveData<Boolean> isSubmittable() {
+ return this.isSubmittable;
+ }
+ void reset() {
+ Logger.debug("new-trans", "Resetting model");
+ List<Item> list = new ArrayList<>();
+ Item.resetIdDispenser();
+ list.add(new TransactionHead(""));
+ final String defaultCurrency = Objects.requireNonNull(Data.getProfile())
+ .getDefaultCommodity();
+ list.add(new TransactionAccount("", defaultCurrency));
+ list.add(new TransactionAccount("", defaultCurrency));
+ noteFocusChanged(0, FocusedElement.Description);
+ renumberItems();
+ isSubmittable.setValue(false);
+ setItemsWithoutSubmittableChecks(list);
+ }
+ boolean accountsInInitialState() {
+ final List<Item> list = items.getValue();
+
+ if (list == null)
+ return true;
+
+ for (Item item : list) {
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ TransactionAccount accRow = (TransactionAccount) item;
+ if (!accRow.isEmpty())
+ return false;
+ }
+
+ return true;
+ }
+ void applyTemplate(MatchedTemplate matchedTemplate, String text) {
+ SimpleDate transactionDate = null;
+ final MatchResult matchResult = matchedTemplate.matchResult;
+ final TemplateHeader templateHead = matchedTemplate.templateHead;
+ {
+ int day = extractIntFromMatches(matchResult, templateHead.getDateDayMatchGroup(),
+ templateHead.getDateDay());
+ int month = extractIntFromMatches(matchResult, templateHead.getDateMonthMatchGroup(),
+ templateHead.getDateMonth());
+ int year = extractIntFromMatches(matchResult, templateHead.getDateYearMatchGroup(),
+ templateHead.getDateYear());
+
+ if (year > 0 || month > 0 || day > 0) {
+ SimpleDate today = SimpleDate.today();
+ if (year <= 0)
+ year = today.year;
+ if (month <= 0)
+ month = today.month;
+ if (day <= 0)
+ day = today.day;
+
+ transactionDate = new SimpleDate(year, month, day);
+
+ Logger.debug("pattern", "setting transaction date to " + transactionDate);
+ }
+ }
+
+ List<Item> present = copyList();
+
+ TransactionHead head = new TransactionHead(present.get(0)
+ .toTransactionHead());
+ if (transactionDate != null)
+ head.setDate(transactionDate);
+
+ final String transactionDescription = extractStringFromMatches(matchResult,
+ templateHead.getTransactionDescriptionMatchGroup(),
+ templateHead.getTransactionDescription());
+ if (Misc.emptyIsNull(transactionDescription) != null)
+ head.setDescription(transactionDescription);
+
+ final String transactionComment = extractStringFromMatches(matchResult,
+ templateHead.getTransactionCommentMatchGroup(),
+ templateHead.getTransactionComment());
+ if (Misc.emptyIsNull(transactionComment) != null)
+ head.setComment(transactionComment);
+
+ List<Item> newItems = new ArrayList<>();
+
+ newItems.add(head);
+
+ for (int i = 1; i < present.size(); i++) {
+ final TransactionAccount row = present.get(i)
+ .toTransactionAccount();
+ if (!row.isEmpty())
+ newItems.add(new TransactionAccount(row));
+ }
+
+ DB.get()
+ .getTemplateDAO()
+ .getTemplateWithAccountsAsync(templateHead.getId(), entry -> {
+ int rowIndex = 0;
+ final boolean accountsInInitialState = accountsInInitialState();
+ for (TemplateAccount acc : entry.accounts) {
+ rowIndex++;
+
+ String accountName =
+ extractStringFromMatches(matchResult, acc.getAccountNameMatchGroup(),
+ acc.getAccountName());
+ String accountComment =
+ extractStringFromMatches(matchResult, acc.getAccountCommentMatchGroup(),
+ acc.getAccountComment());
+ Float amount = extractFloatFromMatches(matchResult, acc.getAmountMatchGroup(),
+ acc.getAmount());
+ if (amount != null && acc.getNegateAmount() != null && acc.getNegateAmount())
+ amount = -amount;
+
+ TransactionAccount accRow = new TransactionAccount(accountName);
+ accRow.setComment(accountComment);
+ if (amount != null)
+ accRow.setAmount(amount);
+ accRow.setCurrency(
+ extractCurrencyFromMatches(matchResult, acc.getCurrencyMatchGroup(),
+ acc.getCurrencyObject()));
+
+ newItems.add(accRow);
+ }
+
+ renumberItems(newItems);
+ Misc.onMainThread(() -> replaceItems(newItems));
+ });
+ }
+ @NonNull
+ private String extractCurrencyFromMatches(MatchResult m, Integer group, Currency literal) {
+ return Misc.nullIsEmpty(
+ extractStringFromMatches(m, group, (literal == null) ? "" : literal.getName()));
+ }
+ private int extractIntFromMatches(MatchResult m, Integer group, Integer literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 && grp <= m.groupCount())
+ try {
+ return Integer.parseInt(m.group(grp));
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", "Error extracting matched number", e);
+ }
+ }
+
+ return 0;
+ }
+ @Nullable
+ private String extractStringFromMatches(MatchResult m, Integer group, String literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 && grp <= m.groupCount())
+ return m.group(grp);
+ }
+
+ return null;
+ }
+ private Float extractFloatFromMatches(MatchResult m, Integer group, Float literal) {
+ if (literal != null)
+ return literal;
+
+ if (group != null) {
+ int grp = group;
+ if (grp > 0 && grp <= m.groupCount())
+ try {
+ return Float.valueOf(m.group(grp));
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", "Error extracting matched number", e);
+ }
+ }
+
+ return null;
+ }
+ void removeItem(int pos) {
+ Logger.debug("new-trans", String.format(Locale.US, "Removing item at position %d", pos));
+ List<Item> newList = copyListWithoutItem(pos);
+ final FocusInfo fi = focusInfo.getValue();
+ if ((fi != null) && (pos < fi.position))
+ noteFocusChanged(fi.position - 1, fi.element);
+ setItems(newList);
+ }
+ void noteFocusChanged(int position, @Nullable FocusedElement element) {
+ FocusInfo present = focusInfo.getValue();
+ if (present == null || present.position != position || present.element != element)
+ focusInfo.setValue(new FocusInfo(position, element));
+ }
+ public LiveData<FocusInfo> getFocusInfo() {
+ return focusInfo;
+ }
+ void moveItem(int fromIndex, int toIndex) {
+ List<Item> newList = shallowCopyList();
+ Item item = newList.remove(fromIndex);
+ newList.add(toIndex, item);
+
+ FocusInfo fi = focusInfo.getValue();
+ if (fi != null && fi.position == fromIndex)
+ noteFocusChanged(toIndex, fi.element);
+
+ items.setValue(newList); // same count, same submittable state
+ }
+ void moveItemLast(List<Item> list, int index) {
+ /* 0
+ 1 <-- index
+ 2
+ 3 <-- desired position
+ (no bottom filler)
+ */
+ int itemCount = list.size();
+
+ if (index < itemCount - 1)
+ list.add(list.remove(index));
+ }
+ void toggleCurrencyVisible() {
+ final boolean newValue = !Objects.requireNonNull(showCurrency.getValue());
+
+ // remove currency from all items, or reset currency to the default
+ // no need to clone the list, because the removal of the currency won't lead to
+ // visual changes -- the currency fields will be hidden or reset to default anyway
+ // still, there may be changes in the submittable state
+ final List<Item> list = Objects.requireNonNull(this.items.getValue());
+ final Profile profile = Objects.requireNonNull(Data.getProfile());
+ for (int i = 1; i < list.size(); i++) {
+ ((TransactionAccount) list.get(i)).setCurrency(
+ newValue ? profile.getDefaultCommodity() : "");
+ }
+ checkTransactionSubmittable(null);
+ showCurrency.setValue(newValue);
+ }
+ void stopObservingBusyFlag(Observer<Boolean> observer) {
+ busyFlag.removeObserver(observer);
+ }
+ void incrementBusyCounter() {
+ int newValue = busyCounter.incrementAndGet();
+ if (newValue == 1)
+ busyFlag.postValue(true);
+ }
+ void decrementBusyCounter() {
+ int newValue = busyCounter.decrementAndGet();
+ if (newValue == 0)
+ busyFlag.postValue(false);
+ }
+ public LiveData<Boolean> getBusyFlag() {
+ return busyFlag;
+ }
+ public void toggleShowComments() {
+ showComments.setValue(!Objects.requireNonNull(showComments.getValue()));
+ }
+ public LedgerTransaction constructLedgerTransaction() {
+ List<Item> list = Objects.requireNonNull(items.getValue());
+ TransactionHead head = list.get(0)
+ .toTransactionHead();
+ LedgerTransaction tr = head.asLedgerTransaction();
+
+ tr.setComment(head.getComment());
+ HashMap<String, List<LedgerTransactionAccount>> emptyAmountAccounts = new HashMap<>();
+ HashMap<String, Float> emptyAmountAccountBalance = new HashMap<>();
+ for (int i = 1; i < list.size(); i++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+ String currency = item.getCurrency();
+ LedgerTransactionAccount acc = new LedgerTransactionAccount(item.getAccountName()
+ .trim(), currency);
+ if (acc.getAccountName()
+ .isEmpty())
+ continue;
+
+ acc.setComment(item.getComment());
+
+ if (item.isAmountSet()) {
+ acc.setAmount(item.getAmount());
+ Float emptyCurrBalance = emptyAmountAccountBalance.get(currency);
+ if (emptyCurrBalance == null) {
+ emptyAmountAccountBalance.put(currency, item.getAmount());
+ }
+ else {
+ emptyAmountAccountBalance.put(currency, emptyCurrBalance + item.getAmount());
+ }
+ }
+ else {
+ List<LedgerTransactionAccount> emptyCurrAccounts =
+ emptyAmountAccounts.get(currency);
+ if (emptyCurrAccounts == null)
+ emptyAmountAccounts.put(currency, emptyCurrAccounts = new ArrayList<>());
+ emptyCurrAccounts.add(acc);
+ }
+
+ tr.addAccount(acc);
+ }
+
+ if (emptyAmountAccounts.size() > 0) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ emptyAmountAccounts.forEach((currency, accounts) -> {
+ final Float balance = emptyAmountAccountBalance.get(currency);
+
+ if (balance != null && !Misc.isZero(balance) && accounts.size() != 1) {
+ throw new RuntimeException(String.format(Locale.US,
+ "Should not happen: approved transaction has %d accounts " +
+ "without amounts for currency '%s'", accounts.size(), currency));
+ }
+ accounts.forEach(acc -> acc.setAmount(balance == null ? 0 : -balance));
+ });
+ }
+ else {
+ for (String currency : emptyAmountAccounts.keySet()) {
+ List<LedgerTransactionAccount> accounts =
+ Objects.requireNonNull(emptyAmountAccounts.get(currency));
+ final Float balance = emptyAmountAccountBalance.get(currency);
+ if (balance != null && !Misc.isZero(balance) && accounts.size() != 1)
+ throw new RuntimeException(String.format(Locale.US,
+ "Should not happen: approved transaction has %d accounts for " +
+ "currency %s", accounts.size(), currency));
+ for (LedgerTransactionAccount acc : accounts) {
+ acc.setAmount(balance == null ? 0 : -balance);
+ }
+ }
+ }
+ }
+
+ return tr;
+ }
+ void loadTransactionIntoModel(@NonNull TransactionWithAccounts tr) {
+ List<Item> newList = new ArrayList<>();
+ Item.resetIdDispenser();
+
+ Item currentHead = Objects.requireNonNull(items.getValue())
+ .get(0);
+ TransactionHead head = new TransactionHead(tr.transaction.getDescription());
+ head.setComment(tr.transaction.getComment());
+ if (currentHead instanceof TransactionHead)
+ head.setDate(((TransactionHead) currentHead).date);
+
+ newList.add(head);
+
+ List<LedgerTransactionAccount> accounts = new ArrayList<>();
+ for (net.ktnx.mobileledger.db.TransactionAccount acc : tr.accounts) {
+ accounts.add(new LedgerTransactionAccount(acc));
+ }
+
+ TransactionAccount firstNegative = null;
+ TransactionAccount firstPositive = null;
+ int singleNegativeIndex = -1;
+ int singlePositiveIndex = -1;
+ int negativeCount = 0;
+ boolean hasCurrency = false;
+ for (int i = 0; i < accounts.size(); i++) {
+ LedgerTransactionAccount acc = accounts.get(i);
+ TransactionAccount item = new TransactionAccount(acc.getAccountName(),
+ Misc.nullIsEmpty(acc.getCurrency()));
+ newList.add(item);
+
+ item.setAccountName(acc.getAccountName());
+ item.setComment(acc.getComment());
+ if (acc.isAmountSet()) {
+ item.setAmount(acc.getAmount());
+ if (acc.getAmount() < 0) {
+ if (firstNegative == null) {
+ firstNegative = item;
+ singleNegativeIndex = i + 1;
+ }
+ else
+ singleNegativeIndex = -1;
+ }
+ else {
+ if (firstPositive == null) {
+ firstPositive = item;
+ singlePositiveIndex = i + 1;
+ }
+ else
+ singlePositiveIndex = -1;
+ }
+ }
+ else
+ item.resetAmount();
+
+ if (item.getCurrency()
+ .length() > 0)
+ hasCurrency = true;
+ }
+ if (BuildConfig.DEBUG)
+ dumpItemList("Loaded previous transaction", newList);
+
+ if (singleNegativeIndex != -1) {
+ firstNegative.resetAmount();
+ moveItemLast(newList, singleNegativeIndex);
+ }
+ else if (singlePositiveIndex != -1) {
+ firstPositive.resetAmount();
+ moveItemLast(newList, singlePositiveIndex);
+ }
+
+ final boolean foundTransactionHasCurrency = hasCurrency;
+ Misc.onMainThread(() -> {
+ setItems(newList);
+ noteFocusChanged(1, FocusedElement.Amount);
+ if (foundTransactionHasCurrency)
+ showCurrency.setValue(true);
+ });
+ }
+ /**
+ * A transaction is submittable if:
+ * 0) has description
+ * 1) has at least two account names
+ * 2) each row with amount has account name
+ * 3) for each commodity:
+ * 3a) amounts must balance to 0, or
+ * 3b) there must be exactly one empty amount (with account)
+ * 4) empty accounts with empty amounts are ignored
+ * Side effects:
+ * 5) a row with an empty account name or empty amount is guaranteed to exist for each
+ * commodity
+ * 6) at least two rows need to be present in the ledger
+ *
+ * @param list - the item list to check. Can be the displayed list or a list that will be
+ * displayed soon
+ */
+ @SuppressLint("DefaultLocale")
+ void checkTransactionSubmittable(@Nullable List<Item> list) {
+ boolean workingWithLiveList = false;
+ if (list == null) {
+ list = copyList();
+ workingWithLiveList = true;
+ }
+
+ if (BuildConfig.DEBUG)
+ dumpItemList(String.format("Before submittable checks (%s)",
+ workingWithLiveList ? "LIVE LIST" : "custom list"), list);
+
+ int accounts = 0;
+ final BalanceForCurrency balance = new BalanceForCurrency();
+ final String descriptionText = list.get(0)
+ .toTransactionHead()
+ .getDescription();
+ boolean submittable = true;
+ boolean listChanged = false;
+ final ItemsForCurrency itemsForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountAndEmptyAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithEmptyAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAmountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency itemsWithAccountForCurrency = new ItemsForCurrency();
+ final ItemsForCurrency emptyRowsForCurrency = new ItemsForCurrency();
+ final List<Item> emptyRows = new ArrayList<>();
+
+ try {
+ if ((descriptionText == null) || descriptionText.trim()
+ .isEmpty())
+ {
+ Logger.debug("submittable", "Transaction not submittable: missing description");
+ submittable = false;
+ }
+
+ boolean hasInvalidAmount = false;
+
+ for (int i = 1; i < list.size(); i++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+
+ String accName = item.getAccountName()
+ .trim();
+ String currName = item.getCurrency();
+
+ itemsForCurrency.add(currName, item);
+
+ if (accName.isEmpty()) {
+ itemsWithEmptyAccountForCurrency.add(currName, item);
+
+ if (item.isAmountSet()) {
+ // 2) each amount has account name
+ Logger.debug("submittable", String.format(
+ "Transaction not submittable: row %d has no account name, but" +
+ " has" + " amount %1.2f", i + 1, item.getAmount()));
+ submittable = false;
+ }
+ else {
+ emptyRowsForCurrency.add(currName, item);
+ }
+ }
+ else {
+ accounts++;
+ itemsWithAccountForCurrency.add(currName, item);
+ }
+
+ if (item.isAmountSet() && item.isAmountValid()) {
+ itemsWithAmountForCurrency.add(currName, item);
+ balance.add(currName, item.getAmount());
+ }
+ else {
+ if (!item.isAmountValid()) {
+ Logger.debug("submittable",
+ String.format("Not submittable: row %d has an invalid amount", i));
+ submittable = false;
+ hasInvalidAmount = true;
+ }
+
+ itemsWithEmptyAmountForCurrency.add(currName, item);
+
+ if (!accName.isEmpty())
+ itemsWithAccountAndEmptyAmountForCurrency.add(currName, item);
+ }
+ }
+
+ // 1) has at least two account names
+ if (accounts < 2) {
+ if (accounts == 0)
+ Logger.debug("submittable", "Transaction not submittable: no account names");
+ else if (accounts == 1)
+ Logger.debug("submittable",
+ "Transaction not submittable: only one account name");
+ else
+ Logger.debug("submittable",
+ String.format("Transaction not submittable: only %d account names",
+ accounts));
+ submittable = false;
+ }
+
+ // 3) for each commodity:
+ // 3a) amount must balance to 0, or
+ // 3b) there must be exactly one empty amount (with account)
+ for (String balCurrency : itemsForCurrency.currencies()) {
+ float currencyBalance = balance.get(balCurrency);
+ if (Misc.isZero(currencyBalance)) {
+ // remove hints from all amount inputs in that currency
+ for (int i = 1; i < list.size(); i++) {
+ TransactionAccount acc = list.get(i)
+ .toTransactionAccount();
+ if (Misc.equalStrings(acc.getCurrency(), balCurrency)) {
+ if (BuildConfig.DEBUG)
+ Logger.debug("submittable",
+ String.format(Locale.US, "Resetting hint of %d:'%s' [%s]",
+ i, Misc.nullIsEmpty(acc.getAccountName()),
+ balCurrency));
+ // skip if the amount is set, in which case the hint is not
+ // important/visible
+ if (!acc.isAmountSet() && acc.amountHintIsSet &&
+ !TextUtils.isEmpty(acc.getAmountHint()))
+ {
+ acc.setAmountHint(null);
+ listChanged = true;
+ }
+ }
+ }
+ }
+ else {
+ List<Item> tmpList =
+ itemsWithAccountAndEmptyAmountForCurrency.getList(balCurrency);
+ int balanceReceiversCount = tmpList.size();
+ if (balanceReceiversCount != 1) {
+ if (BuildConfig.DEBUG) {
+ if (balanceReceiversCount == 0)
+ Logger.debug("submittable", String.format(
+ "Transaction not submittable [curr:%s]: non-zero balance " +
+ "with no empty amounts with accounts", balCurrency));
+ else
+ Logger.debug("submittable", String.format(
+ "Transaction not submittable [curr:%s]: non-zero balance " +
+ "with multiple empty amounts with accounts", balCurrency));
+ }
+ submittable = false;
+ }
+
+ List<Item> emptyAmountList =
+ itemsWithEmptyAmountForCurrency.getList(balCurrency);
+
+ // suggest off-balance amount to a row and remove hints on other rows
+ Item receiver = null;
+ if (!tmpList.isEmpty())
+ receiver = tmpList.get(0);
+ else if (!emptyAmountList.isEmpty())
+ receiver = emptyAmountList.get(0);
+
+ for (int i = 0; i < list.size(); i++) {
+ Item item = list.get(i);
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ TransactionAccount acc = item.toTransactionAccount();
+ if (!Misc.equalStrings(acc.getCurrency(), balCurrency))
+ continue;
+
+ if (item == receiver) {
+ final String hint = Data.formatNumber(-currencyBalance);
+ if (!acc.isAmountHintSet() ||
+ !Misc.equalStrings(acc.getAmountHint(), hint))
+ {
+ Logger.debug("submittable",
+ String.format("Setting amount hint of {%s} to %s [%s]", acc,
+ hint, balCurrency));
+ acc.setAmountHint(hint);
+ listChanged = true;
+ }
+ }
+ else {
+ if (BuildConfig.DEBUG)
+ Logger.debug("submittable",
+ String.format("Resetting hint of '%s' [%s]",
+ Misc.nullIsEmpty(acc.getAccountName()),
+ balCurrency));
+ if (acc.amountHintIsSet && !TextUtils.isEmpty(acc.getAmountHint())) {
+ acc.setAmountHint(null);
+ listChanged = true;
+ }
+ }
+ }
+ }
+ }
+
+ // 5) a row with an empty account name or empty amount is guaranteed to exist for
+ // each commodity
+ if (!hasInvalidAmount) {
+ for (String balCurrency : balance.currencies()) {
+ int currEmptyRows = itemsWithEmptyAccountForCurrency.size(balCurrency);
+ int currRows = itemsForCurrency.size(balCurrency);
+ int currAccounts = itemsWithAccountForCurrency.size(balCurrency);
+ int currAmounts = itemsWithAmountForCurrency.size(balCurrency);
+ if ((currEmptyRows == 0) &&
+ ((currRows == currAccounts) || (currRows == currAmounts)))
+ {
+ // perhaps there already is an unused empty row for another currency that
+ // is not used?
+// boolean foundIt = false;
+// for (Item item : emptyRows) {
+// Currency itemCurrency = item.getCurrency();
+// String itemCurrencyName =
+// (itemCurrency == null) ? "" : itemCurrency.getName();
+// if (Misc.isZero(balance.get(itemCurrencyName))) {
+// item.setCurrency(Currency.loadByName(balCurrency));
+// item.setAmountHint(
+// Data.formatNumber(-balance.get(balCurrency)));
+// foundIt = true;
+// break;
+// }
+// }
+//
+// if (!foundIt)
+ final TransactionAccount newAcc = new TransactionAccount("", balCurrency);
+ final float bal = balance.get(balCurrency);
+ if (!Misc.isZero(bal) && currAmounts == currRows)
+ newAcc.setAmountHint(Data.formatNumber(-bal));
+ Logger.debug("submittable",
+ String.format("Adding new item with %s for currency %s",
+ newAcc.getAmountHint(), balCurrency));
+ list.add(newAcc);
+ listChanged = true;
+ }
+ }
+ }
+
+ // drop extra empty rows, not needed
+ for (String currName : emptyRowsForCurrency.currencies()) {
+ List<Item> emptyItems = emptyRowsForCurrency.getList(currName);
+ while ((list.size() > MIN_ITEMS) && (emptyItems.size() > 1)) {
+ // the list is a copy, so the empty item is no longer present
+ Item itemToRemove = emptyItems.remove(1);
+ removeItemById(list, itemToRemove.id);
+ listChanged = true;
+ }
+
+ // unused currency, remove last item (which is also an empty one)
+ if ((list.size() > MIN_ITEMS) && (emptyItems.size() == 1)) {
+ List<Item> currItems = itemsForCurrency.getList(currName);
+
+ if (currItems.size() == 1) {
+ // the list is a copy, so the empty item is no longer present
+ removeItemById(list, emptyItems.get(0).id);
+ listChanged = true;
+ }
+ }
+ }
+
+ // 6) at least two rows need to be present in the ledger
+ // (the list also contains header and trailer)
+ while (list.size() < MIN_ITEMS) {
+ list.add(new TransactionAccount(""));
+ listChanged = true;
+ }
+
+ Logger.debug("submittable", submittable ? "YES" : "NO");
+ isSubmittable.setValue(submittable);
+
+ if (BuildConfig.DEBUG)
+ dumpItemList("After submittable checks", list);
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("submittable", "NO (because of NumberFormatException)");
+ isSubmittable.setValue(false);
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ Logger.debug("submittable", "NO (because of an Exception)");
+ isSubmittable.setValue(false);
+ }
+
+ if (listChanged && workingWithLiveList) {
+ setItemsWithoutSubmittableChecks(list);
+ }
+ }
+ private void removeItemById(@NotNull List<Item> list, int id) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
+ list.removeIf(item -> item.id == id);
+ }
+ else {
+ for (Item item : list) {
+ if (item.id == id) {
+ list.remove(item);
+ break;
+ }
+ }
+ }
+ }
+ @SuppressLint("DefaultLocale")
+ private void dumpItemList(@NotNull String msg, @NotNull List<Item> list) {
+ Logger.debug("submittable", "== Dump of all items " + msg);
+ for (int i = 1; i < list.size(); i++) {
+ TransactionAccount item = list.get(i)
+ .toTransactionAccount();
+ Logger.debug("submittable", String.format("%d:%s", i, item.toString()));
+ }
+ }
+ public void setItemCurrency(int position, String newCurrency) {
+ TransactionAccount item = Objects.requireNonNull(items.getValue())
+ .get(position)
+ .toTransactionAccount();
+ final String oldCurrency = item.getCurrency();
+
+ if (Misc.equalStrings(oldCurrency, newCurrency))
+ return;
+
+ List<Item> newList = copyList();
+ newList.get(position)
+ .toTransactionAccount()
+ .setCurrency(newCurrency);
+
+ setItems(newList);
+ }
+ public boolean accountListIsEmpty() {
+ List<Item> items = Objects.requireNonNull(this.items.getValue());
+
+ for (Item item : items) {
+ if (!(item instanceof TransactionAccount))
+ continue;
+
+ if (!((TransactionAccount) item).isEmpty())
+ return false;
+ }
+
+ return true;
+ }
+
+ public static class FocusInfo {
+ int position;
+ FocusedElement element;
+ public FocusInfo(int position, @Nullable FocusedElement element) {
+ this.position = position;
+ this.element = element;
+ }
+ }
+
+ static abstract class Item {
+ private static int idDispenser = 0;
+ protected int id;
+ private Item() {
+ if (this instanceof TransactionHead)
+ id = 0;
+ else
+ synchronized (Item.class) {
+ id = ++idDispenser;
+ }
+ }
+ public Item(int id) {
+ this.id = id;
+ }
+ public static Item from(Item origin) {
+ if (origin instanceof TransactionHead)
+ return new TransactionHead((TransactionHead) origin);
+ if (origin instanceof TransactionAccount)
+ return new TransactionAccount((TransactionAccount) origin);
+ throw new RuntimeException("Don't know how to handle " + origin);
+ }
+ private static void resetIdDispenser() {
+ idDispenser = 0;
+ }
+ public int getId() {
+ return id;
+ }
+ public abstract ItemType getType();
+ public TransactionHead toTransactionHead() {
+ if (this instanceof TransactionHead)
+ return (TransactionHead) this;
+
+ throw new IllegalStateException("Wrong item type " + this);
+ }
+ public TransactionAccount toTransactionAccount() {
+ if (this instanceof TransactionAccount)
+ return (TransactionAccount) this;
+
+ throw new IllegalStateException("Wrong item type " + this);
+ }
+ public boolean equalContents(@Nullable Object item) {
+ if (item == null)
+ return false;
+
+ if (!getClass().equals(item.getClass()))
+ return false;
+
+ // shortcut - comparing same instance
+ if (item == this)
+ return true;
+
+ if (this instanceof TransactionHead)
+ return ((TransactionHead) item).equalContents((TransactionHead) this);
+ if (this instanceof TransactionAccount)
+ return ((TransactionAccount) item).equalContents((TransactionAccount) this);
+
+ throw new RuntimeException("Don't know how to handle " + this);
+ }
+ }
+
+
+//==========================================================================================
+
+ public static class TransactionHead extends Item {
+ private SimpleDate date;
+ private String description;
+ private String comment;
+ TransactionHead(String description) {
+ super();
+ this.description = description;
+ }
+ public TransactionHead(TransactionHead origin) {
+ super(origin.id);
+ date = origin.date;
+ description = origin.description;
+ comment = origin.comment;
+ }
+ public SimpleDate getDate() {
+ return date;
+ }
+ public void setDate(SimpleDate date) {
+ this.date = date;
+ }
+ public void setDate(String text) throws ParseException {
+ if (Misc.emptyIsNull(text) == null) {
+ date = null;
+ return;
+ }
+
+ date = Globals.parseLedgerDate(text);
+ }
+ /**
+ * getFormattedDate()
+ *
+ * @return nicely formatted, shortest available date representation
+ */
+ String getFormattedDate() {
+ if (date == null)
+ return null;
+
+ Calendar today = GregorianCalendar.getInstance();
+
+ if (today.get(Calendar.YEAR) != date.year) {
+ return String.format(Locale.US, "%d/%02d/%02d", date.year, date.month, date.day);
+ }
+
+ if (today.get(Calendar.MONTH) + 1 != date.month) {
+ return String.format(Locale.US, "%d/%02d", date.month, date.day);
+ }
+
+ return String.valueOf(date.day);
+ }
+ @NonNull
+ @Override
+ public String toString() {
+ @SuppressLint("DefaultLocale") StringBuilder b = new StringBuilder(
+ String.format("id:%d/%s", id, Integer.toHexString(hashCode())));
+
+ if (TextUtils.isEmpty(description))
+ b.append(" «no description»");
+ else
+ b.append(String.format(" '%s'", description));
+
+ if (date != null)
+ b.append(String.format("@%s", date));
+
+ if (!TextUtils.isEmpty(comment))
+ b.append(String.format(" /%s/", comment));
+
+ return b.toString();
+ }
+ public String getDescription() {
+ return description;
+ }
+ public void setDescription(String description) {
+ this.description = description;
+ }
+ public String getComment() {
+ return comment;
+ }
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+ @Override
+ public ItemType getType() {
+ return ItemType.generalData;
+ }
+ public LedgerTransaction asLedgerTransaction() {
+ return new LedgerTransaction(0, (date == null) ? SimpleDate.today() : date, description,
+ Objects.requireNonNull(Data.getProfile()));
+ }
+ public boolean equalContents(TransactionHead other) {
+ if (other == null)
+ return false;
+
+ return Objects.equals(date, other.date) &&
+ Misc.equalStrings(description, other.description) &&
+ Misc.equalStrings(comment, other.comment);
+ }
+ }
+
+ public static class TransactionAccount extends Item {
+ private String accountName;
+ private String amountHint;
+ private String comment;
+ @NotNull
+ private String currency = "";
+ private float amount;
+ private boolean amountSet;
+ private boolean amountValid = true;
+ @NotNull
+ private String amountText = "";
+ private FocusedElement focusedElement = FocusedElement.Account;
+ private boolean amountHintIsSet = false;
+ private boolean isLast = false;
+ private int accountNameCursorPosition;
+ public TransactionAccount(TransactionAccount origin) {
+ super(origin.id);
+ accountName = origin.accountName;
+ amount = origin.amount;
+ amountSet = origin.amountSet;
+ amountHint = origin.amountHint;
+ amountHintIsSet = origin.amountHintIsSet;
+ amountText = origin.amountText;
+ comment = origin.comment;
+ currency = origin.currency;
+ amountValid = origin.amountValid;
+ focusedElement = origin.focusedElement;
+ isLast = origin.isLast;
+ accountNameCursorPosition = origin.accountNameCursorPosition;
+ }
+ public TransactionAccount(String accountName) {
+ super();
+ this.accountName = accountName;
+ }
+ public TransactionAccount(String accountName, @NotNull String currency) {
+ super();
+ this.accountName = accountName;
+ this.currency = currency;
+ }
+ public @NotNull String getAmountText() {
+ return amountText;
+ }
+ public void setAmountText(@NotNull String amountText) {
+ this.amountText = amountText;
+ }
+ public boolean setAndCheckAmountText(@NotNull String amountText) {
+ String amtText = amountText.trim();
+ this.amountText = amtText;
+
+ boolean significantChange = false;
+
+ if (amtText.isEmpty()) {
+ if (amountSet) {
+ significantChange = true;
+ }
+ resetAmount();
+ }
+ else {
+ try {
+ amtText = amtText.replace(Data.getDecimalSeparator(), Data.decimalDot);
+ final float parsedAmount = Float.parseFloat(amtText);
+ if (!amountSet || !amountValid || !Misc.equalFloats(parsedAmount, amount))
+ significantChange = true;
+ amount = parsedAmount;
+ amountSet = true;
+ amountValid = true;
+ }
+ catch (NumberFormatException e) {
+ Logger.debug("new-trans", String.format(
+ "assuming amount is not set due to number format exception. " +
+ "input was '%s'", amtText));
+ if (amountValid) // it was valid and now it's not
+ significantChange = true;
+ amountValid = false;
+ }
+ }
+
+ return significantChange;
+ }
+ public boolean isLast() {
+ return isLast;
+ }
+ public boolean isAmountSet() {
+ return amountSet;
+ }
+ public String getAccountName() {
+ return accountName;
+ }
+ public void setAccountName(String accountName) {
+ this.accountName = accountName;
+ }
+ public float getAmount() {
+ if (!amountSet)
+ throw new IllegalStateException("Amount is not set");
+ return amount;
+ }
+ public void setAmount(float amount) {
+ this.amount = amount;
+ amountSet = true;
+ amountValid = true;
+ amountText = Data.formatNumber(amount);
+ }
+ public void resetAmount() {
+ amountSet = false;
+ amountValid = true;
+ amountText = "";
+ }
+ @Override
+ public ItemType getType() {
+ return ItemType.transactionRow;
+ }
+ public String getAmountHint() {
+ return amountHint;
+ }
+ public void setAmountHint(String amountHint) {
+ this.amountHint = amountHint;
+ amountHintIsSet = !TextUtils.isEmpty(amountHint);
+ }
+ public String getComment() {
+ return comment;
+ }
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+ @NotNull
+ public String getCurrency() {
+ return currency;
+ }
+ public void setCurrency(@org.jetbrains.annotations.Nullable String currency) {
+ this.currency = Misc.nullIsEmpty(currency);
+ }
+ public boolean isAmountValid() {
+ return amountValid;
+ }
+ public void setAmountValid(boolean amountValid) {
+ this.amountValid = amountValid;
+ }
+ public FocusedElement getFocusedElement() {
+ return focusedElement;
+ }
+ public void setFocusedElement(FocusedElement focusedElement) {
+ this.focusedElement = focusedElement;
+ }
+ public boolean isAmountHintSet() {
+ return amountHintIsSet;
+ }
+ public void setAmountHintIsSet(boolean amountHintIsSet) {
+ this.amountHintIsSet = amountHintIsSet;
+ }
+ public boolean isEmpty() {
+ return !amountSet && Misc.emptyIsNull(accountName) == null &&
+ Misc.emptyIsNull(comment) == null;
+ }
+ @SuppressLint("DefaultLocale")
+ @Override
+ @NotNull
+ public String toString() {
+ StringBuilder b = new StringBuilder();
+ b.append(String.format("id:%d/%s", id, Integer.toHexString(hashCode())));
+ if (!TextUtils.isEmpty(accountName))
+ b.append(String.format(" acc'%s'", accountName));
+
+ if (amountSet)
+ b.append(amountText)
+ .append(" [")
+ .append(amountValid ? "valid" : "invalid")
+ .append("] ")
+ .append(String.format(Locale.ROOT, " {raw %4.2f}", amount));
+ else if (amountHintIsSet)
+ b.append(String.format(" (hint %s)", amountHint));
+
+ if (!TextUtils.isEmpty(currency))
+ b.append(" ")
+ .append(currency);
+
+ if (!TextUtils.isEmpty(comment))
+ b.append(String.format(" /%s/", comment));
+
+ if (isLast)
+ b.append(" last");
+
+ return b.toString();
+ }
+ public boolean equalContents(TransactionAccount other) {
+ if (other == null)
+ return false;
+
+ boolean equal = Misc.equalStrings(accountName, other.accountName);
+ equal = equal && Misc.equalStrings(comment, other.comment) &&
+ (amountSet ? other.amountSet && amountValid == other.amountValid &&
+ Misc.equalStrings(amountText, other.amountText)
+ : !other.amountSet);
+
+ // compare amount hint only if there is no amount
+ if (!amountSet)
+ equal = equal && (amountHintIsSet ? other.amountHintIsSet &&
+ Misc.equalStrings(amountHint, other.amountHint)
+ : !other.amountHintIsSet);
+ equal = equal && Misc.equalStrings(currency, other.currency) && isLast == other.isLast;
+
+ Logger.debug("new-trans",
+ String.format("Comparing {%s} and {%s}: %s", this, other, equal));
+ return equal;
+ }
+ public int getAccountNameCursorPosition() {
+ return accountNameCursorPosition;
+ }
+ public void setAccountNameCursorPosition(int position) {
+ this.accountNameCursorPosition = position;
+ }
+ }
+
+ private static class BalanceForCurrency {
+ private final HashMap<String, Float> hashMap = new HashMap<>();
+ float get(String currencyName) {
+ Float f = hashMap.get(currencyName);
+ if (f == null) {
+ f = 0f;
+ hashMap.put(currencyName, f);
+ }
+ return f;
+ }
+ void add(String currencyName, float amount) {
+ hashMap.put(currencyName, get(currencyName) + amount);
+ }
+ Set<String> currencies() {
+ return hashMap.keySet();
+ }
+ boolean containsCurrency(String currencyName) {
+ return hashMap.containsKey(currencyName);
+ }
+ }
+
+ private static class ItemsForCurrency {
+ private final HashMap<@NotNull String, List<Item>> hashMap = new HashMap<>();
+ @NonNull
+ List<NewTransactionModel.Item> getList(@NotNull String currencyName) {
+ List<NewTransactionModel.Item> list = hashMap.get(currencyName);
+ if (list == null) {
+ list = new ArrayList<>();
+ hashMap.put(currencyName, list);
+ }
+ return list;
+ }
+ void add(@NotNull String currencyName, @NonNull NewTransactionModel.Item item) {
+ getList(Objects.requireNonNull(currencyName)).add(item);
+ }
+ int size(@NotNull String currencyName) {
+ return this.getList(Objects.requireNonNull(currencyName))
+ .size();
+ }
+ Set<String> currencies() {
+ return hashMap.keySet();
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.profiles;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.widget.Toolbar;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Locale;
+
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
+/**
+ * An activity representing a single Profile detail screen. This
+ * activity is only used on narrow width devices. On tablet-size devices,
+ * item details are presented side-by-side with a list of items
+ * in a ProfileListActivity (not really).
+ */
+public class ProfileDetailActivity extends CrashReportingActivity {
+ private static final String TAG = "profile-det-act";
+ private ProfileDetailFragment mFragment;
+ public static void start(Context context, @Nullable Profile profile) {
+ Intent starter = new Intent(context, ProfileDetailActivity.class);
+ if (profile != null) {
+ starter.putExtra(ProfileDetailFragment.ARG_ITEM_ID, profile.getId());
+ starter.putExtra(ProfileDetailFragment.ARG_HUE, profile.getTheme());
+ Logger.debug(TAG,
+ String.format(Locale.ROOT, "Starting profile editor for profile %d, theme %d",
+ profile.getId(), profile.getTheme()));
+ }
+ else
+ Logger.debug(TAG, "Starting empty profile editor");
+ context.startActivity(starter);
+ }
+ @NotNull
+ private ProfileDetailModel getModel() {
+ return new ViewModelProvider(this).get(ProfileDetailModel.class);
+ }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ final long id = getIntent().getLongExtra(ProfileDetailFragment.ARG_ITEM_ID, -1);
+
+ DB.get()
+ .getProfileDAO()
+ .getById(id)
+ .observe(this, this::setProfile);
+
+ int themeHue = getIntent().getIntExtra(ProfileDetailFragment.ARG_HUE, -1);
+
+ super.onCreate(savedInstanceState);
+ if (themeHue == -1) {
+ themeHue = Colors.getNewProfileThemeHue(Data.profiles.getValue());
+ }
+ Colors.setupTheme(this, themeHue);
+ final ProfileDetailModel model = getModel();
+ model.initialThemeHue = themeHue;
+ model.setThemeId(themeHue);
+ setContentView(R.layout.activity_profile_detail);
+ Toolbar toolbar = findViewById(R.id.detail_toolbar);
+ setSupportActionBar(toolbar);
+
+
+ // Show the Up button in the action bar.
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ // savedInstanceState is non-null when there is fragment state
+ // saved from previous configurations of this activity
+ // (e.g. when rotating the screen from portrait to landscape).
+ // In this case, the fragment will automatically be re-added
+ // to its container so we don't need to manually add it.
+ // For more information, see the Fragments API guide at:
+ //
+ // http://developer.android.com/guide/components/fragments.html
+ //
+ if (savedInstanceState == null) {
+ // Create the detail fragment and add it to the activity
+ // using a fragment transaction.
+ Bundle arguments = new Bundle();
+ arguments.putInt(ProfileDetailFragment.ARG_HUE, themeHue);
+ mFragment = new ProfileDetailFragment();
+ mFragment.setArguments(arguments);
+ getSupportFragmentManager().beginTransaction()
+ .add(R.id.profile_detail_container, mFragment)
+ .commit();
+ }
+ }
+ private void setProfile(Profile profile) {
+ ProfileDetailModel model = new ViewModelProvider(this).get(ProfileDetailModel.class);
+ CollapsingToolbarLayout appBarLayout = findViewById(R.id.toolbar_layout);
+ if (appBarLayout != null) {
+ if (profile != null)
+ appBarLayout.setTitle(profile.getName());
+ else
+ appBarLayout.setTitle(getResources().getString(R.string.new_profile_title));
+ }
+ model.setValuesFromProfile(profile);
+ }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ debug("profiles", "[activity] Creating profile details options menu");
+ if (mFragment != null)
+ mFragment.onCreateOptionsMenu(menu, getMenuInflater());
+
+ return true;
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.backup.BackupManager;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
+import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
-import android.widget.LinearLayout;
+import android.view.ViewGroup;
import android.widget.PopupMenu;
-import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
-import com.google.android.material.appbar.CollapsingToolbarLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.textfield.TextInputLayout;
import net.ktnx.mobileledger.BuildConfig;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.SendTransactionTask;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.dao.ProfileDAO;
+import net.ktnx.mobileledger.databinding.ProfileDetailBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.json.API;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.model.FutureDates;
import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
import net.ktnx.mobileledger.ui.HueRingDialog;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Misc;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
-import java.util.UUID;
import static net.ktnx.mobileledger.utils.Logger.debug;
public static final String ARG_HUE = "hue";
@NonNls
- private MobileLedgerProfile mProfile;
- private TextView url;
- private TextView defaultCommodity;
private boolean defaultCommoditySet;
- private TextInputLayout urlLayout;
- private LinearLayout authParams;
- private Switch useAuthentication;
- private TextView userName;
- private TextInputLayout userNameLayout;
- private TextView password;
- private TextInputLayout passwordLayout;
- private TextView profileName;
- private TextInputLayout profileNameLayout;
- private TextView preferredAccountsFilter;
- private View huePickerView;
- private View insecureWarningText;
- private TextView futureDatesText;
- private TextView apiVersionText;
private boolean syncingModelFromUI = false;
+ private ProfileDetailBinding binding;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
inflater.inflate(R.menu.profile_details, menu);
final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete);
menuDeleteProfile.setOnMenuItemClickListener(item -> onDeleteProfile());
- final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
+ final List<Profile> profiles = Data.profiles.getValue();
- if (BuildConfig.DEBUG) {
- final MenuItem menuWipeProfileData = menu.findItem(R.id.menuWipeData);
+ final MenuItem menuWipeProfileData = menu.findItem(R.id.menuWipeData);
+ if (BuildConfig.DEBUG)
menuWipeProfileData.setOnMenuItemClickListener(ignored -> onWipeDataMenuClicked());
- menuWipeProfileData.setVisible(mProfile != null);
- }
+
+ getModel().getProfileId()
+ .observe(getViewLifecycleOwner(), id -> {
+ menuDeleteProfile.setVisible(id > 0);
+ if (BuildConfig.DEBUG)
+ menuWipeProfileData.setVisible(id > 0);
+ });
}
private boolean onDeleteProfile() {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setTitle(mProfile.getName());
+ @NotNull ProfileDetailModel model = getModel();
+ builder.setTitle(model.getProfileName());
builder.setMessage(R.string.remove_profile_dialog_message);
builder.setPositiveButton(R.string.Remove, (dialog, which) -> {
- debug("profiles", String.format("[fragment] removing profile %s", mProfile.getUuid()));
- mProfile.removeFromDB();
- ArrayList<MobileLedgerProfile> oldList = Data.profiles.getValue();
- if (oldList == null)
- throw new AssertionError();
- ArrayList<MobileLedgerProfile> newList = new ArrayList<>(oldList);
- newList.remove(mProfile);
- Data.profiles.setValue(newList);
- if (mProfile.equals(Data.getProfile())) {
- debug("profiles", "[fragment] setting current profile to 0");
- Data.setCurrentProfile(newList.get(0));
- }
+ final long profileId = Objects.requireNonNull(model.getProfileId()
+ .getValue());
+ debug("profiles", String.format("[fragment] removing profile %s", profileId));
+ ProfileDAO dao = DB.get()
+ .getProfileDAO();
+ dao.getById(profileId)
+ .observe(getViewLifecycleOwner(), profile -> {
+ if (profile != null)
+ BaseDAO.runAsync(() -> DB.get()
+ .runInTransaction(() -> {
+ dao.deleteSync(profile);
+ dao.updateOrderSync(dao.getAllOrderedSync());
+ }));
+ });
final FragmentActivity activity = getActivity();
if (activity != null)
}
private boolean onWipeDataMenuClicked() {
// this is a development option, so no confirmation
- mProfile.wipeAllData();
- if (mProfile.equals(Data.getProfile()))
- triggerProfileChange();
+ DB.get()
+ .getProfileDAO()
+ .getById(Objects.requireNonNull(getModel().getProfileId()
+ .getValue()))
+ .observe(getViewLifecycleOwner(), profile -> {
+ if (profile != null)
+ profile.wipeAllData();
+ });
return true;
}
- private void triggerProfileChange() {
- int index = Data.getProfileIndex(mProfile);
- MobileLedgerProfile newProfile = new MobileLedgerProfile(mProfile);
- final ArrayList<MobileLedgerProfile> profiles =
- Objects.requireNonNull(Data.profiles.getValue());
- profiles.set(index, newProfile);
-
- ProfilesRecyclerViewAdapter viewAdapter = ProfilesRecyclerViewAdapter.getInstance();
- if (viewAdapter != null)
- viewAdapter.notifyItemChanged(index);
-
- if (mProfile.equals(Data.getProfile()))
- Data.setCurrentProfile(newProfile);
- }
private void hookTextChangeSyncRoutine(TextView view, TextChangeSyncRoutine syncRoutine) {
view.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) { syncRoutine.onTextChanged(s.toString());}
});
}
+ @Nullable
@Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ binding = ProfileDetailBinding.inflate(inflater, container, false);
+
+ return binding.getRoot();
+ }
+ @Override
+ public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
Activity context = getActivity();
if (context == null)
return;
- if ((getArguments() != null) && getArguments().containsKey(ARG_ITEM_ID)) {
- int index = getArguments().getInt(ARG_ITEM_ID, -1);
- ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
- if ((profiles != null) && (index != -1) && (index < profiles.size()))
- mProfile = profiles.get(index);
-
- Activity activity = this.getActivity();
- if (activity == null)
- throw new AssertionError();
- CollapsingToolbarLayout appBarLayout = activity.findViewById(R.id.toolbar_layout);
- if (appBarLayout != null) {
- if (mProfile != null)
- appBarLayout.setTitle(mProfile.getName());
- else
- appBarLayout.setTitle(getResources().getString(R.string.new_profile_title));
- }
- }
-
final LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
final ProfileDetailModel model = getModel();
model.observeDefaultCommodity(viewLifecycleOwner, c -> {
if (c != null)
- setDefaultCommodity(c.getName());
+ setDefaultCommodity(c);
else
resetDefaultCommodity();
});
- FloatingActionButton fab = context.findViewById(R.id.fab);
+ FloatingActionButton fab = context.findViewById(R.id.fabAdd);
fab.setOnClickListener(v -> onSaveFabClicked());
- profileName = context.findViewById(R.id.profile_name);
- hookTextChangeSyncRoutine(profileName, model::setProfileName);
+ hookTextChangeSyncRoutine(binding.profileName, model::setProfileName);
model.observeProfileName(viewLifecycleOwner, pn -> {
- if (!Misc.equalStrings(pn, profileName.getText()))
- profileName.setText(pn);
+ if (!Misc.equalStrings(pn, Misc.nullIsEmpty(binding.profileName.getText())))
+ binding.profileName.setText(pn);
});
- profileNameLayout = context.findViewById(R.id.profile_name_layout);
-
- url = context.findViewById(R.id.url);
- hookTextChangeSyncRoutine(url, model::setUrl);
+ hookTextChangeSyncRoutine(binding.url, model::setUrl);
model.observeUrl(viewLifecycleOwner, u -> {
- if (!Misc.equalStrings(u, url.getText()))
- url.setText(u);
+ if (!Misc.equalStrings(u, Misc.nullIsEmpty(binding.url.getText())))
+ binding.url.setText(u);
});
- urlLayout = context.findViewById(R.id.url_layout);
-
- context.findViewById(R.id.default_commodity_layout)
- .setOnClickListener(v -> {
- CurrencySelectorFragment cpf = CurrencySelectorFragment.newInstance(
- CurrencySelectorFragment.DEFAULT_COLUMN_COUNT, false);
- cpf.setOnCurrencySelectedListener(model::setDefaultCommodity);
- final AppCompatActivity activity = (AppCompatActivity) v.getContext();
- cpf.show(activity.getSupportFragmentManager(), "currency-selector");
- });
+ binding.defaultCommodityLayout.setOnClickListener(v -> {
+ CurrencySelectorFragment cpf = CurrencySelectorFragment.newInstance(
+ CurrencySelectorFragment.DEFAULT_COLUMN_COUNT, false);
+ cpf.setOnCurrencySelectedListener(model::setDefaultCommodity);
+ final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ cpf.show(activity.getSupportFragmentManager(), "currency-selector");
+ });
- Switch showCommodityByDefault = context.findViewById(R.id.profile_show_commodity);
- showCommodityByDefault.setOnCheckedChangeListener(
+ binding.profileShowCommodity.setOnCheckedChangeListener(
(buttonView, isChecked) -> model.setShowCommodityByDefault(isChecked));
- model.observeShowCommodityByDefault(viewLifecycleOwner, showCommodityByDefault::setChecked);
-
- View postingSubItems = context.findViewById(R.id.posting_sub_items);
+ model.observeShowCommodityByDefault(viewLifecycleOwner,
+ binding.profileShowCommodity::setChecked);
- Switch postingPermitted = context.findViewById(R.id.profile_permit_posting);
model.observePostingPermitted(viewLifecycleOwner, isChecked -> {
- postingPermitted.setChecked(isChecked);
- postingSubItems.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+ binding.profilePermitPosting.setChecked(isChecked);
+ binding.postingSubItems.setVisibility(isChecked ? View.VISIBLE : View.GONE);
});
- postingPermitted.setOnCheckedChangeListener(
+ binding.profilePermitPosting.setOnCheckedChangeListener(
((buttonView, isChecked) -> model.setPostingPermitted(isChecked)));
- Switch showCommentsByDefault = context.findViewById(R.id.profile_show_comments);
- model.observeShowCommentsByDefault(viewLifecycleOwner, showCommentsByDefault::setChecked);
- showCommentsByDefault.setOnCheckedChangeListener(
+ model.observeShowCommentsByDefault(viewLifecycleOwner,
+ binding.profileShowComments::setChecked);
+ binding.profileShowComments.setOnCheckedChangeListener(
((buttonView, isChecked) -> model.setShowCommentsByDefault(isChecked)));
- defaultCommodity = context.findViewById(R.id.default_commodity_text);
-
- futureDatesText = context.findViewById(R.id.future_dates_text);
- context.findViewById(R.id.future_dates_layout)
- .setOnClickListener(v -> {
- MenuInflater mi = new MenuInflater(context);
- PopupMenu menu = new PopupMenu(context, v);
- menu.inflate(R.menu.future_dates);
- menu.setOnMenuItemClickListener(item -> {
- model.setFutureDates(futureDatesSettingFromMenuItemId(item.getItemId()));
- return true;
- });
- menu.show();
- });
+ binding.futureDatesLayout.setOnClickListener(v -> {
+ MenuInflater mi = new MenuInflater(context);
+ PopupMenu menu = new PopupMenu(context, v);
+ menu.inflate(R.menu.future_dates);
+ menu.setOnMenuItemClickListener(item -> {
+ model.setFutureDates(futureDatesSettingFromMenuItemId(item.getItemId()));
+ return true;
+ });
+ menu.show();
+ });
model.observeFutureDates(viewLifecycleOwner,
- v -> futureDatesText.setText(v.getText(getResources())));
+ v -> binding.futureDatesText.setText(v.getText(getResources())));
- apiVersionText = context.findViewById(R.id.api_version_text);
model.observeApiVersion(viewLifecycleOwner,
- apiVer -> apiVersionText.setText(apiVer.getDescription(getResources())));
- context.findViewById(R.id.api_version_label)
- .setOnClickListener(this::chooseAPIVersion);
- context.findViewById(R.id.api_version_text)
- .setOnClickListener(this::chooseAPIVersion);
+ apiVer -> binding.apiVersionText.setText(apiVer.getDescription(getResources())));
+ binding.apiVersionLabel.setOnClickListener(this::chooseAPIVersion);
+ binding.apiVersionText.setOnClickListener(this::chooseAPIVersion);
- TextView detectedApiVersion = context.findViewById(R.id.detected_version_text);
+ binding.serverVersionLabel.setOnClickListener(v -> model.triggerVersionDetection());
model.observeDetectedVersion(viewLifecycleOwner, ver -> {
if (ver == null)
- detectedApiVersion.setText(context.getResources()
- .getString(R.string.api_version_unknown_label));
- else if (ver.isPre_1_20())
- detectedApiVersion.setText(context.getResources()
- .getString(R.string.api_pre_1_19));
+ binding.detectedServerVersionText.setText(context.getResources()
+ .getString(
+ R.string.server_version_unknown_label));
+ else if (ver.isPre_1_20_1())
+ binding.detectedServerVersionText.setText(context.getResources()
+ .getString(
+ R.string.detected_server_pre_1_20_1));
else
- detectedApiVersion.setText(ver.toString());
+ binding.detectedServerVersionText.setText(ver.toString());
});
- detectedApiVersion.setOnClickListener(v -> model.triggerVersionDetection());
- context.findViewById(R.id.api_version_detect_button)
- .setOnClickListener(v -> model.triggerVersionDetection());
-
- authParams = context.findViewById(R.id.auth_params);
-
- useAuthentication = context.findViewById(R.id.enable_http_auth);
- useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ binding.detectedServerVersionText.setOnClickListener(v -> model.triggerVersionDetection());
+ binding.serverVersionDetectButton.setOnClickListener(v -> model.triggerVersionDetection());
+ model.observeDetectingHledgerVersion(viewLifecycleOwner,
+ running -> binding.serverVersionDetectButton.setVisibility(
+ running ? View.VISIBLE : View.INVISIBLE));
+
+ binding.enableHttpAuth.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ boolean wasOn = model.getUseAuthentication();
model.setUseAuthentication(isChecked);
- if (isChecked)
- userName.requestFocus();
+ if (!wasOn && isChecked)
+ binding.authUserName.requestFocus();
});
model.observeUseAuthentication(viewLifecycleOwner, isChecked -> {
- useAuthentication.setChecked(isChecked);
- authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+ binding.enableHttpAuth.setChecked(isChecked);
+ binding.authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
checkInsecureSchemeWithAuth();
});
- userName = context.findViewById(R.id.auth_user_name);
model.observeUserName(viewLifecycleOwner, text -> {
- if (!Misc.equalStrings(text, userName.getText()))
- userName.setText(text);
+ if (!Misc.equalStrings(text, Misc.nullIsEmpty(binding.authUserName.getText())))
+ binding.authUserName.setText(text);
});
- hookTextChangeSyncRoutine(userName, model::setAuthUserName);
- userNameLayout = context.findViewById(R.id.auth_user_name_layout);
+ hookTextChangeSyncRoutine(binding.authUserName, model::setAuthUserName);
- password = context.findViewById(R.id.password);
model.observePassword(viewLifecycleOwner, text -> {
- if (!Misc.equalStrings(text, password.getText()))
- password.setText(text);
+ if (!Misc.equalStrings(text, Misc.nullIsEmpty(binding.password.getText())))
+ binding.password.setText(text);
});
- hookTextChangeSyncRoutine(password, model::setAuthPassword);
- passwordLayout = context.findViewById(R.id.password_layout);
+ hookTextChangeSyncRoutine(binding.password, model::setAuthPassword);
- huePickerView = context.findViewById(R.id.btn_pick_ring_color);
model.observeThemeId(viewLifecycleOwner, themeId -> {
final int hue = (themeId == -1) ? Colors.DEFAULT_HUE_DEG : themeId;
final int profileColor = Colors.getPrimaryColorForHue(hue);
- huePickerView.setBackgroundColor(profileColor);
- huePickerView.setTag(hue);
+ binding.btnPickRingColor.setBackgroundColor(profileColor);
+ binding.btnPickRingColor.setTag(hue);
});
- preferredAccountsFilter = context.findViewById(R.id.preferred_accounts_filter_filter);
model.observePreferredAccountsFilter(viewLifecycleOwner, text -> {
- if (!Misc.equalStrings(text, preferredAccountsFilter.getText()))
- preferredAccountsFilter.setText(text);
+ if (!Misc.equalStrings(text,
+ Misc.nullIsEmpty(binding.preferredAccountsFilter.getText())))
+ binding.preferredAccountsFilter.setText(text);
});
- hookTextChangeSyncRoutine(preferredAccountsFilter, model::setPreferredAccountsFilter);
-
- insecureWarningText = context.findViewById(R.id.insecure_scheme_text);
+ hookTextChangeSyncRoutine(binding.preferredAccountsFilter,
+ model::setPreferredAccountsFilter);
- hookClearErrorOnFocusListener(profileName, profileNameLayout);
- hookClearErrorOnFocusListener(url, urlLayout);
- hookClearErrorOnFocusListener(userName, userNameLayout);
- hookClearErrorOnFocusListener(password, passwordLayout);
+ hookClearErrorOnFocusListener(binding.profileName, binding.profileNameLayout);
+ hookClearErrorOnFocusListener(binding.url, binding.urlLayout);
+ hookClearErrorOnFocusListener(binding.authUserName, binding.authUserNameLayout);
+ hookClearErrorOnFocusListener(binding.password, binding.passwordLayout);
- if (savedInstanceState == null) {
- model.setValuesFromProfile(mProfile, getArguments().getInt(ARG_HUE, -1));
- }
- checkInsecureSchemeWithAuth();
-
- url.addTextChangedListener(new TextWatcher() {
+ binding.url.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
}
});
- huePickerView.setOnClickListener(v -> {
+ binding.btnPickRingColor.setOnClickListener(v -> {
HueRingDialog d = new HueRingDialog(ProfileDetailFragment.this.requireContext(),
model.initialThemeHue, (Integer) v.getTag());
d.show();
d.setColorSelectedListener(model::setThemeId);
});
- profileName.requestFocus();
+ binding.profileName.requestFocus();
}
private void chooseAPIVersion(View v) {
Activity context = getActivity();
PopupMenu menu = new PopupMenu(context, v);
menu.inflate(R.menu.api_version);
menu.setOnMenuItemClickListener(item -> {
- SendTransactionTask.API apiVer;
- switch (item.getItemId()) {
- case R.id.api_version_menu_html:
- apiVer = SendTransactionTask.API.html;
- break;
- case R.id.api_version_menu_post_1_14:
- apiVer = SendTransactionTask.API.post_1_14;
- break;
- case R.id.api_version_menu_pre_1_15:
- apiVer = SendTransactionTask.API.pre_1_15;
- break;
- case R.id.api_version_menu_auto:
- default:
- apiVer = SendTransactionTask.API.auto;
+ API apiVer;
+ int itemId = item.getItemId();
+ if (itemId == R.id.api_version_menu_html) {
+ apiVer = API.html;
+ }
+ else if (itemId == R.id.api_version_menu_1_23) {
+ apiVer = API.v1_23;
+ }
+ else if (itemId == R.id.api_version_menu_1_19_1) {
+ apiVer = API.v1_19_1;
+ }
+ else if (itemId == R.id.api_version_menu_1_15) {
+ apiVer = API.v1_15;
+ }
+ else if (itemId == R.id.api_version_menu_1_14) {
+ apiVer = API.v1_14;
+ }
+ else {
+ apiVer = API.auto;
}
model.setApiVersion(apiVer);
- apiVersionText.setText(apiVer.getDescription(getResources()));
+ binding.apiVersionText.setText(apiVer.getDescription(getResources()));
return true;
});
menu.show();
}
- private MobileLedgerProfile.FutureDates futureDatesSettingFromMenuItemId(int itemId) {
- switch (itemId) {
- case R.id.menu_future_dates_7:
- return MobileLedgerProfile.FutureDates.OneWeek;
- case R.id.menu_future_dates_14:
- return MobileLedgerProfile.FutureDates.TwoWeeks;
- case R.id.menu_future_dates_30:
- return MobileLedgerProfile.FutureDates.OneMonth;
- case R.id.menu_future_dates_60:
- return MobileLedgerProfile.FutureDates.TwoMonths;
- case R.id.menu_future_dates_90:
- return MobileLedgerProfile.FutureDates.ThreeMonths;
- case R.id.menu_future_dates_180:
- return MobileLedgerProfile.FutureDates.SixMonths;
- case R.id.menu_future_dates_365:
- return MobileLedgerProfile.FutureDates.OneYear;
- case R.id.menu_future_dates_all:
- return MobileLedgerProfile.FutureDates.All;
- default:
- return MobileLedgerProfile.FutureDates.None;
+ private FutureDates futureDatesSettingFromMenuItemId(int itemId) {
+ if (itemId == R.id.menu_future_dates_7) {
+ return FutureDates.OneWeek;
+ }
+ else if (itemId == R.id.menu_future_dates_14) {
+ return FutureDates.TwoWeeks;
+ }
+ else if (itemId == R.id.menu_future_dates_30) {
+ return FutureDates.OneMonth;
+ }
+ else if (itemId == R.id.menu_future_dates_60) {
+ return FutureDates.TwoMonths;
}
+ else if (itemId == R.id.menu_future_dates_90) {
+ return FutureDates.ThreeMonths;
+ }
+ else if (itemId == R.id.menu_future_dates_180) {
+ return FutureDates.SixMonths;
+ }
+ else if (itemId == R.id.menu_future_dates_365) {
+ return FutureDates.OneYear;
+ }
+ else if (itemId == R.id.menu_future_dates_all) {
+ return FutureDates.All;
+ }
+ return FutureDates.None;
}
@NotNull
private ProfileDetailModel getModel() {
return;
ProfileDetailModel model = getModel();
- final ArrayList<MobileLedgerProfile> profiles =
- Objects.requireNonNull(Data.profiles.getValue());
-
- if (mProfile != null) {
- int pos = Data.profiles.getValue()
- .indexOf(mProfile);
- mProfile = new MobileLedgerProfile(mProfile);
- model.updateProfile(mProfile);
- mProfile.storeInDB();
+ ProfileDAO dao = DB.get()
+ .getProfileDAO();
+
+ Profile profile = new Profile();
+ model.updateProfile(profile);
+ if (profile.getId() > 0) {
+ dao.update(profile);
debug("profiles", "profile stored in DB");
- profiles.set(pos, mProfile);
// debug("profiles", String.format("Selected item is %d", mProfile.getThemeHue()));
-
- final MobileLedgerProfile currentProfile = Data.getProfile();
- if (mProfile.getUuid()
- .equals(currentProfile.getUuid()))
- {
- Data.setCurrentProfile(mProfile);
- }
-
- ProfilesRecyclerViewAdapter viewAdapter = ProfilesRecyclerViewAdapter.getInstance();
- if (viewAdapter != null)
- viewAdapter.notifyItemChanged(pos);
}
else {
- mProfile = new MobileLedgerProfile(String.valueOf(UUID.randomUUID()));
- model.updateProfile(mProfile);
- mProfile.storeInDB();
- final ArrayList<MobileLedgerProfile> newList = new ArrayList<>(profiles);
- newList.add(mProfile);
- Data.profiles.setValue(newList);
- MobileLedgerProfile.storeProfilesOrder();
-
- // first profile ever?
- if (newList.size() == 1)
- Data.setCurrentProfile(mProfile);
+ dao.insertLast(profile, null);
}
+ BackupManager.dataChanged(BuildConfig.APPLICATION_ID);
+
Activity activity = getActivity();
if (activity != null)
activity.finish();
.trim();
if (val.isEmpty()) {
valid = false;
- urlLayout.setError(getResources().getText(R.string.err_profile_url_empty));
+ binding.urlLayout.setError(getResources().getText(R.string.err_profile_url_empty));
}
try {
URL url = new URL(val);
.toUpperCase();
if (!protocol.equals("HTTP") && !protocol.equals("HTTPS")) {
valid = false;
- urlLayout.setError(getResources().getText(R.string.err_invalid_url));
+ binding.urlLayout.setError(getResources().getText(R.string.err_invalid_url));
}
}
catch (MalformedURLException e) {
valid = false;
- urlLayout.setError(getResources().getText(R.string.err_invalid_url));
+ binding.urlLayout.setError(getResources().getText(R.string.err_invalid_url));
}
return valid;
if (model.getUseAuthentication()) {
String urlText = model.getUrl();
- if (urlText.startsWith("http") && !urlText.startsWith("https"))
+ if (urlText.startsWith("http://") ||
+ urlText.length() >= 8 && !urlText.startsWith("https://"))
showWarning = true;
}
if (showWarning)
- insecureWarningText.setVisibility(View.VISIBLE);
+ binding.insecureSchemeText.setVisibility(View.VISIBLE);
else
- insecureWarningText.setVisibility(View.GONE);
+ binding.insecureSchemeText.setVisibility(View.GONE);
}
private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
view.setOnFocusChangeListener((v, hasFocus) -> {
try {
ProfileDetailModel model = getModel();
- model.setProfileName(profileName.getText());
- model.setUrl(url.getText());
- model.setPreferredAccountsFilter(preferredAccountsFilter.getText());
- model.setAuthUserName(userName.getText());
- model.setAuthPassword(password.getText());
+ model.setProfileName(binding.profileName.getText());
+ model.setUrl(binding.url.getText());
+ model.setPreferredAccountsFilter(binding.preferredAccountsFilter.getText());
+ model.setAuthUserName(binding.authUserName.getText());
+ model.setAuthPassword(binding.password.getText());
}
finally {
syncingModelFromUI = false;
private boolean checkValidity() {
boolean valid = true;
- String val = String.valueOf(profileName.getText());
+ String val = String.valueOf(binding.profileName.getText());
if (val.trim()
.isEmpty())
{
valid = false;
- profileNameLayout.setError(getResources().getText(R.string.err_profile_name_empty));
+ binding.profileNameLayout.setError(
+ getResources().getText(R.string.err_profile_name_empty));
}
if (!checkUrlValidity())
valid = false;
- if (useAuthentication.isChecked()) {
- val = String.valueOf(userName.getText());
+ if (binding.enableHttpAuth.isChecked()) {
+ val = String.valueOf(binding.authUserName.getText());
if (val.trim()
.isEmpty())
{
valid = false;
- userNameLayout.setError(
+ binding.authUserNameLayout.setError(
getResources().getText(R.string.err_profile_user_name_empty));
}
- val = String.valueOf(password.getText());
+ val = String.valueOf(binding.password.getText());
if (val.trim()
.isEmpty())
{
valid = false;
- passwordLayout.setError(
+ binding.passwordLayout.setError(
getResources().getText(R.string.err_profile_password_empty));
}
}
}
private void resetDefaultCommodity() {
defaultCommoditySet = false;
- defaultCommodity.setText(R.string.btn_no_currency);
- defaultCommodity.setTypeface(defaultCommodity.getTypeface(), Typeface.ITALIC);
+ binding.defaultCommodityText.setText(R.string.btn_no_currency);
+ binding.defaultCommodityText.setTypeface(binding.defaultCommodityText.getTypeface(),
+ Typeface.ITALIC);
}
private void setDefaultCommodity(@NonNull @NotNull String name) {
defaultCommoditySet = true;
- defaultCommodity.setText(name);
- defaultCommodity.setTypeface(Typeface.DEFAULT);
+ binding.defaultCommodityText.setText(name);
+ binding.defaultCommodityText.setTypeface(Typeface.DEFAULT);
}
interface TextChangeSyncRoutine {
void onTextChanged(String text);
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import android.text.TextUtils;
import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModel;
-import net.ktnx.mobileledger.async.SendTransactionTask;
-import net.ktnx.mobileledger.model.Currency;
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.db.Profile;
+import net.ktnx.mobileledger.json.API;
+import net.ktnx.mobileledger.model.FutureDates;
import net.ktnx.mobileledger.model.HledgerVersion;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.Misc;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import static net.ktnx.mobileledger.db.Profile.NO_PROFILE_ID;
+
public class ProfileDetailModel extends ViewModel {
private static final String HTTPS_URL_START = "https://";
private final MutableLiveData<String> profileName = new MutableLiveData<>();
+ private final MutableLiveData<Integer> orderNo = new MutableLiveData<>();
private final MutableLiveData<Boolean> postingPermitted = new MutableLiveData<>(true);
- private final MutableLiveData<Currency> defaultCommodity = new MutableLiveData<>(null);
- private final MutableLiveData<MobileLedgerProfile.FutureDates> futureDates =
- new MutableLiveData<>(MobileLedgerProfile.FutureDates.None);
+ private final MutableLiveData<String> defaultCommodity = new MutableLiveData<>(null);
+ private final MutableLiveData<FutureDates> futureDates =
+ new MutableLiveData<>(FutureDates.None);
private final MutableLiveData<Boolean> showCommodityByDefault = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> showCommentsByDefault = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> useAuthentication = new MutableLiveData<>(false);
- private final MutableLiveData<SendTransactionTask.API> apiVersion =
- new MutableLiveData<>(SendTransactionTask.API.auto);
+ private final MutableLiveData<API> apiVersion = new MutableLiveData<>(API.auto);
private final MutableLiveData<String> url = new MutableLiveData<>(null);
private final MutableLiveData<String> authUserName = new MutableLiveData<>(null);
private final MutableLiveData<String> authPassword = new MutableLiveData<>(null);
private final MutableLiveData<String> preferredAccountsFilter = new MutableLiveData<>(null);
private final MutableLiveData<Integer> themeId = new MutableLiveData<>(-1);
private final MutableLiveData<HledgerVersion> detectedVersion = new MutableLiveData<>(null);
+ private final MutableLiveData<Boolean> detectingHledgerVersion = new MutableLiveData<>(false);
+ private final MutableLiveData<Long> profileId = new MutableLiveData<>(NO_PROFILE_ID);
public int initialThemeHue = Colors.DEFAULT_HUE_DEG;
private VersionDetectionThread versionDetectionThread;
public ProfileDetailModel() {
void observeShowCommentsByDefault(LifecycleOwner lfo, Observer<Boolean> o) {
showCommentsByDefault.observe(lfo, o);
}
- MobileLedgerProfile.FutureDates getFutureDates() {
+ FutureDates getFutureDates() {
return futureDates.getValue();
}
- void setFutureDates(MobileLedgerProfile.FutureDates newValue) {
+ void setFutureDates(FutureDates newValue) {
if (newValue != futureDates.getValue())
futureDates.setValue(newValue);
}
- void observeFutureDates(LifecycleOwner lfo, Observer<MobileLedgerProfile.FutureDates> o) {
+ void observeFutureDates(LifecycleOwner lfo, Observer<FutureDates> o) {
futureDates.observe(lfo, o);
}
- Currency getDefaultCommodity() {
+ String getDefaultCommodity() {
return defaultCommodity.getValue();
}
- void setDefaultCommodity(Currency newValue) {
- if (newValue != defaultCommodity.getValue())
+ void setDefaultCommodity(String newValue) {
+ if (!Misc.equalStrings(newValue, defaultCommodity.getValue()))
defaultCommodity.setValue(newValue);
}
- void observeDefaultCommodity(LifecycleOwner lfo, Observer<Currency> o) {
+ void observeDefaultCommodity(LifecycleOwner lfo, Observer<String> o) {
defaultCommodity.observe(lfo, o);
}
Boolean getShowCommodityByDefault() {
void observeShowCommodityByDefault(LifecycleOwner lfo, Observer<Boolean> o) {
showCommodityByDefault.observe(lfo, o);
}
- Boolean getUseAuthentication() {
+ public Boolean getUseAuthentication() {
return useAuthentication.getValue();
}
void setUseAuthentication(boolean newValue) {
void observeUseAuthentication(LifecycleOwner lfo, Observer<Boolean> o) {
useAuthentication.observe(lfo, o);
}
- SendTransactionTask.API getApiVersion() {
+ API getApiVersion() {
return apiVersion.getValue();
}
- void setApiVersion(SendTransactionTask.API newValue) {
+ void setApiVersion(API newValue) {
if (newValue != apiVersion.getValue())
apiVersion.setValue(newValue);
}
- void observeApiVersion(LifecycleOwner lfo, Observer<SendTransactionTask.API> o) {
+ void observeApiVersion(LifecycleOwner lfo, Observer<API> o) {
apiVersion.observe(lfo, o);
}
HledgerVersion getDetectedVersion() { return detectedVersion.getValue(); }
void observeDetectedVersion(LifecycleOwner lfo, Observer<HledgerVersion> o) {
detectedVersion.observe(lfo, o);
}
- String getUrl() {
+ public String getUrl() {
return url.getValue();
}
void setUrl(String newValue) {
void observeUrl(LifecycleOwner lfo, Observer<String> o) {
url.observe(lfo, o);
}
- String getAuthUserName() {
+ public String getAuthUserName() {
return authUserName.getValue();
}
void setAuthUserName(String newValue) {
void observeUserName(LifecycleOwner lfo, Observer<String> o) {
authUserName.observe(lfo, o);
}
- String getAuthPassword() {
+ public String getAuthPassword() {
return authPassword.getValue();
}
void setAuthPassword(String newValue) {
void observeThemeId(LifecycleOwner lfo, Observer<Integer> o) {
themeId.observe(lfo, o);
}
- void setValuesFromProfile(MobileLedgerProfile mProfile, int newProfileHue) {
- final int profileThemeId;
+ void observeDetectingHledgerVersion(LifecycleOwner lfo, Observer<Boolean> o) {
+ detectingHledgerVersion.observe(lfo, o);
+ }
+ void setValuesFromProfile(Profile mProfile) {
if (mProfile != null) {
+ profileId.setValue(mProfile.getId());
profileName.setValue(mProfile.getName());
- postingPermitted.setValue(mProfile.isPostingPermitted());
+ orderNo.setValue(mProfile.getOrderNo());
+ postingPermitted.setValue(mProfile.permitPosting());
showCommentsByDefault.setValue(mProfile.getShowCommentsByDefault());
showCommodityByDefault.setValue(mProfile.getShowCommodityByDefault());
{
if (TextUtils.isEmpty(comm))
setDefaultCommodity(null);
else
- setDefaultCommodity(new Currency(-1, comm));
+ setDefaultCommodity(comm);
}
- futureDates.setValue(mProfile.getFutureDates());
- apiVersion.setValue(mProfile.getApiVersion());
+ futureDates.setValue(FutureDates.valueOf(mProfile.getFutureDates()));
+ apiVersion.setValue(API.valueOf(mProfile.getApiVersion()));
url.setValue(mProfile.getUrl());
- useAuthentication.setValue(mProfile.isAuthEnabled());
- authUserName.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : "");
- authPassword.setValue(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
+ useAuthentication.setValue(mProfile.useAuthentication());
+ authUserName.setValue(mProfile.useAuthentication() ? mProfile.getAuthUser() : "");
+ authPassword.setValue(mProfile.useAuthentication() ? mProfile.getAuthPassword() : "");
preferredAccountsFilter.setValue(mProfile.getPreferredAccountsFilter());
- themeId.setValue(mProfile.getThemeHue());
- detectedVersion.setValue(mProfile.getDetectedVersion());
+ themeId.setValue(mProfile.getTheme());
+ detectedVersion.setValue(mProfile.detectedVersionPre_1_19() ? new HledgerVersion(true)
+ : new HledgerVersion(
+ mProfile.getDetectedVersionMajor(),
+ mProfile.getDetectedVersionMinor()));
}
else {
+ profileId.setValue(NO_PROFILE_ID);
+ orderNo.setValue(-1);
profileName.setValue(null);
url.setValue(HTTPS_URL_START);
postingPermitted.setValue(true);
showCommentsByDefault.setValue(true);
showCommodityByDefault.setValue(false);
- setFutureDates(MobileLedgerProfile.FutureDates.None);
- setApiVersion(SendTransactionTask.API.auto);
+ setFutureDates(FutureDates.None);
+ setApiVersion(API.auto);
useAuthentication.setValue(false);
authUserName.setValue("");
authPassword.setValue("");
preferredAccountsFilter.setValue(null);
- themeId.setValue(newProfileHue);
detectedVersion.setValue(null);
}
}
- void updateProfile(MobileLedgerProfile mProfile) {
+ void updateProfile(Profile mProfile) {
+ mProfile.setId(profileId.getValue());
mProfile.setName(profileName.getValue());
+ mProfile.setOrderNo(orderNo.getValue());
mProfile.setUrl(url.getValue());
- mProfile.setPostingPermitted(postingPermitted.getValue());
+ mProfile.setPermitPosting(postingPermitted.getValue());
mProfile.setShowCommentsByDefault(showCommentsByDefault.getValue());
- Currency c = defaultCommodity.getValue();
- mProfile.setDefaultCommodity((c == null) ? null : c.getName());
+ mProfile.setDefaultCommodity(defaultCommodity.getValue());
mProfile.setShowCommodityByDefault(showCommodityByDefault.getValue());
mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getValue());
- mProfile.setAuthEnabled(useAuthentication.getValue());
- mProfile.setAuthUserName(authUserName.getValue());
+ mProfile.setUseAuthentication(useAuthentication.getValue());
+ mProfile.setAuthUser(authUserName.getValue());
mProfile.setAuthPassword(authPassword.getValue());
- mProfile.setThemeHue(themeId.getValue());
- mProfile.setFutureDates(futureDates.getValue());
- mProfile.setApiVersion(apiVersion.getValue());
- mProfile.setDetectedVersion(detectedVersion.getValue());
+ mProfile.setTheme(themeId.getValue());
+ mProfile.setFutureDates(futureDates.getValue()
+ .toInt());
+ mProfile.setApiVersion(apiVersion.getValue()
+ .toInt());
+ HledgerVersion version = detectedVersion.getValue();
+ mProfile.setDetectedVersionPre_1_19(version != null && version.isPre_1_20_1());
+ mProfile.setDetectedVersionMajor(version != null ? version.getMajor() : -1);
+ mProfile.setDetectedVersionMinor(version != null ? version.getMinor() : -1);
}
synchronized public void triggerVersionDetection() {
if (versionDetectionThread != null)
versionDetectionThread = new VersionDetectionThread(this);
versionDetectionThread.start();
}
+ public LiveData<Long> getProfileId() {
+ return profileId;
+ }
static class VersionDetectionThread extends Thread {
+ static final int TARGET_PROCESS_DURATION = 1000;
private final Pattern versionPattern =
Pattern.compile("^\"(\\d+)\\.(\\d+)(?:\\.(\\d+))?\"$");
private final ProfileDetailModel model;
public VersionDetectionThread(ProfileDetailModel model) {
this.model = model;
}
- @Override
- public void run() {
+ private HledgerVersion detectVersion() {
+ App.setAuthenticationDataFromProfileModel(model);
+ HttpURLConnection http;
try {
- HttpURLConnection http = NetworkUtil.prepareConnection(model.getUrl(), "version",
+ http = NetworkUtil.prepareConnection(model.getUrl(), "version",
model.getUseAuthentication());
switch (http.getResponseCode()) {
case 200:
break;
case 404:
- model.detectedVersion.postValue(new HledgerVersion(true));
- return;
+ return new HledgerVersion(true);
default:
Logger.debug("profile", String.format(Locale.US,
"HTTP error detecting hledger-web version: [%d] %s",
http.getResponseCode(), http.getResponseMessage()));
- model.detectedVersion.postValue(null);
- return;
+ return null;
}
InputStream stream = http.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
if (m.matches()) {
int major = Integer.parseInt(Objects.requireNonNull(m.group(1)));
int minor = Integer.parseInt(Objects.requireNonNull(m.group(2)));
- final boolean hasPatch = m.groupCount() >= 3;
- int patch = hasPatch ? Integer.parseInt(Objects.requireNonNull(m.group(3))) : 0;
+ final String patchText = m.group(3);
+ final boolean hasPatch = patchText != null;
+ int patch = hasPatch ? Integer.parseInt(patchText) : 0;
- model.detectedVersion.postValue(
- hasPatch ? new HledgerVersion(major, minor, patch)
- : new HledgerVersion(major, minor));
+ return hasPatch ? new HledgerVersion(major, minor, patch)
+ : new HledgerVersion(major, minor);
}
else {
Logger.debug("profile",
String.format("Unrecognised version string '%s'", version));
+ return null;
}
}
catch (IOException e) {
e.printStackTrace();
+ return null;
+ }
+ finally {
+ App.resetAuthenticationData();
+ }
+ }
+ @Override
+ public void run() {
+ model.detectingHledgerVersion.postValue(true);
+ try {
+ long startTime = System.currentTimeMillis();
+
+ final HledgerVersion version = detectVersion();
+
+ long elapsed = System.currentTimeMillis() - startTime;
+ Logger.debug("profile", "Detection duration " + elapsed);
+ if (elapsed < TARGET_PROCESS_DURATION) {
+ try {
+ Thread.sleep(TARGET_PROCESS_DURATION - elapsed);
+ }
+ catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ model.detectedVersion.postValue(version);
+ }
+ finally {
+ model.detectingHledgerVersion.postValue(false);
}
}
}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.profiles;
-import android.content.Context;
-import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.lifecycle.MutableLiveData;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
import net.ktnx.mobileledger.utils.Colors;
-import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import static net.ktnx.mobileledger.utils.Logger.debug;
public class ProfilesRecyclerViewAdapter
extends RecyclerView.Adapter<ProfilesRecyclerViewAdapter.ProfileListViewHolder> {
- private static WeakReference<ProfilesRecyclerViewAdapter> instanceRef;
public final MutableLiveData<Boolean> editingProfiles = new MutableLiveData<>(false);
- private final View.OnClickListener mOnClickListener = view -> {
- MobileLedgerProfile profile = (MobileLedgerProfile) ((View) view.getParent()).getTag();
- editProfile(view, profile);
- };
private final ItemTouchHelper rearrangeHelper;
+ private final AsyncListDiffer<Profile> listDiffer;
private RecyclerView recyclerView;
private boolean animationsEnabled = true;
+
public ProfilesRecyclerViewAdapter() {
- instanceRef = new WeakReference<>(this);
debug("flow", "ProfilesRecyclerViewAdapter.new()");
+ setHasStableIds(true);
+ listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<Profile>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull Profile oldItem, @NonNull Profile newItem) {
+ return oldItem.getId() == newItem.getId();
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull Profile oldItem, @NonNull Profile newItem) {
+ return oldItem.equals(newItem);
+ }
+ });
+
ItemTouchHelper.Callback cb = new ItemTouchHelper.Callback() {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView,
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
- final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
- if (profiles == null)
- throw new AssertionError();
- Collections.swap(profiles, viewHolder.getAdapterPosition(),
- target.getAdapterPosition());
- MobileLedgerProfile.storeProfilesOrder();
- notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
+ final List<Profile> profiles = new ArrayList<>(listDiffer.getCurrentList());
+ Collections.swap(profiles, viewHolder.getBindingAdapterPosition(),
+ target.getBindingAdapterPosition());
+ DB.get()
+ .getProfileDAO()
+ .updateOrder(profiles, null);
+// notifyItemMoved(viewHolder.getBindingAdapterPosition(), target
+// .getBindingAdapterPosition());
return true;
}
@Override
};
rearrangeHelper = new ItemTouchHelper(cb);
}
- public static @Nullable
- ProfilesRecyclerViewAdapter getInstance() {
- return instanceRef.get();
+ @Override
+ public long getItemId(int position) {
+ return listDiffer.getCurrentList()
+ .get(position)
+ .getId();
+ }
+ public void setProfileList(List<Profile> list) {
+ listDiffer.submitList(list);
}
public void setAnimationsEnabled(boolean animationsEnabled) {
this.animationsEnabled = animationsEnabled;
else
startEditingProfiles();
}
- private void editProfile(View view, MobileLedgerProfile profile) {
- int index = Data.getProfileIndex(profile);
- Context context = view.getContext();
- Intent intent = new Intent(context, ProfileDetailActivity.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
- if (index != -1)
- intent.putExtra(ProfileDetailFragment.ARG_ITEM_ID, index);
-
- context.startActivity(intent);
- }
- private void onProfileRowClicked(View v) {
- if (editingProfiles())
- return;
- MobileLedgerProfile profile = (MobileLedgerProfile) v.getTag();
- if (profile == null)
- throw new IllegalStateException("Profile row without associated profile");
- debug("profiles", "Setting profile to " + profile.getName());
- if (Data.getProfile() != profile)
- Data.drawerOpen.setValue(false);
- Data.setCurrentProfile(profile);
- }
@NonNull
@Override
public ProfileListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.profile_list_content, parent, false);
- ProfileListViewHolder holder = new ProfileListViewHolder(view);
-
- holder.mRow.setOnClickListener(this::onProfileRowClicked);
- holder.mTitle.setOnClickListener(v -> {
- View row = (View) v.getParent();
- onProfileRowClicked(row);
- });
- holder.mColorTag.setOnClickListener(v -> {
- View row = (View) v.getParent()
- .getParent();
- onProfileRowClicked(row);
- });
- holder.mTitle.setOnLongClickListener(v -> {
- flipEditingProfiles();
- return true;
- });
-
- View.OnTouchListener dragStarter = (v, event) -> {
- if (rearrangeHelper != null && editingProfiles()) {
- rearrangeHelper.startDrag(holder);
- return true;
- }
- return false;
- };
-
- holder.tagAndHandleLayout.setOnTouchListener(dragStarter);
- return holder;
+ return new ProfileListViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ProfileListViewHolder holder, int position) {
- final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
- if (profiles == null)
- throw new AssertionError();
- final MobileLedgerProfile profile = profiles.get(position);
- final MobileLedgerProfile currentProfile = Data.getProfile();
+ final Profile profile = listDiffer.getCurrentList()
+ .get(position);
+ final Profile currentProfile = Data.getProfile();
// debug("profiles", String.format(Locale.ENGLISH, "pos %d: %s, current: %s", position,
// profile.getUuid(), currentProfile.getUuid()));
- holder.itemView.setTag(profile);
- int hue = profile.getThemeHue();
+ int hue = profile.getTheme();
if (hue == -1)
holder.mColorTag.setBackgroundColor(
Colors.getPrimaryColorForHue(Colors.DEFAULT_HUE_DEG));
holder.mTitle.setText(profile.getName());
// holder.mSubTitle.setText(profile.getUrl());
- holder.mEditButton.setOnClickListener(mOnClickListener);
+ holder.mEditButton.setOnClickListener(view -> {
+ Profile p = listDiffer.getCurrentList()
+ .get(holder.getBindingAdapterPosition());
+ ProfileDetailActivity.start(view.getContext(), p);
+ });
- final boolean sameProfile = currentProfile.equals(profile);
+ final boolean sameProfile =
+ currentProfile != null && currentProfile.getId() == profile.getId();
holder.itemView.setBackground(
sameProfile ? new ColorDrawable(Colors.tableRowDarkBG) : null);
if (editingProfiles()) {
}
@Override
public int getItemCount() {
- final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
- return profiles != null ? profiles.size() : 0;
+ return listDiffer.getCurrentList()
+ .size();
}
- static class ProfileListViewHolder extends RecyclerView.ViewHolder {
+ class ProfileListViewHolder extends RecyclerView.ViewHolder {
final TextView mEditButton;
final TextView mTitle, mColorTag;
final LinearLayout tagAndHandleLayout;
mRearrangeHandle = view.findViewById(R.id.profile_list_rearrange_handle);
tagAndHandleLayout = view.findViewById(R.id.handle_and_tag);
mRow = (ConstraintLayout) view;
+
+
+ mRow.setOnClickListener(this::onProfileRowClicked);
+ mTitle.setOnClickListener(v -> {
+ View row = (View) v.getParent();
+ onProfileRowClicked(row);
+ });
+ mColorTag.setOnClickListener(v -> {
+ View row = (View) v.getParent()
+ .getParent();
+ onProfileRowClicked(row);
+ });
+ mTitle.setOnLongClickListener(v -> {
+ flipEditingProfiles();
+ return true;
+ });
+
+ View.OnTouchListener dragStarter = (v, event) -> {
+ if (rearrangeHelper != null && editingProfiles()) {
+ rearrangeHelper.startDrag(this);
+ return true;
+ }
+ return false;
+ };
+
+ tagAndHandleLayout.setOnTouchListener(dragStarter);
+ }
+ private void onProfileRowClicked(View v) {
+ if (editingProfiles())
+ return;
+ Profile profile = listDiffer.getCurrentList()
+ .get(getBindingAdapterPosition());
+ if (Data.getProfile() != profile) {
+ debug("profiles", "Setting profile to " + profile.getName());
+ Data.drawerOpen.setValue(false);
+ Data.setCurrentProfile(profile);
+ }
+ else
+ debug("profiles",
+ "Not setting profile to the current profile " + profile.getName());
}
+
}
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.TemplateDetailsAccountBinding;
+import net.ktnx.mobileledger.databinding.TemplateDetailsHeaderBinding;
+import net.ktnx.mobileledger.db.AccountAutocompleteAdapter;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.model.TemplateDetailsItem;
+import net.ktnx.mobileledger.ui.CurrencySelectorFragment;
+import net.ktnx.mobileledger.ui.HelpDialog;
+import net.ktnx.mobileledger.ui.QR;
+import net.ktnx.mobileledger.ui.TemplateDetailSourceSelectorFragment;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.text.ParseException;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class TemplateDetailsAdapter extends RecyclerView.Adapter<TemplateDetailsAdapter.ViewHolder> {
+ private static final String D_TEMPLATE_UI = "template-ui";
+ private final AsyncListDiffer<TemplateDetailsItem> differ;
+ private final TemplateDetailsViewModel mModel;
+ private final ItemTouchHelper itemTouchHelper;
+ public TemplateDetailsAdapter(TemplateDetailsViewModel model) {
+ super();
+ mModel = model;
+ setHasStableIds(true);
+ differ = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TemplateDetailsItem>() {
+ @Override
+ public boolean areItemsTheSame(@NonNull TemplateDetailsItem oldItem,
+ @NonNull TemplateDetailsItem newItem) {
+ if (oldItem.getType() != newItem.getType())
+ return false;
+ if (oldItem.getType()
+ .equals(TemplateDetailsItem.Type.HEADER))
+ return true; // only one header item, ever
+ // the rest is comparing two account row items
+ return oldItem.asAccountRowItem()
+ .getId() == newItem.asAccountRowItem()
+ .getId();
+ }
+ @Override
+ public boolean areContentsTheSame(@NonNull TemplateDetailsItem oldItem,
+ @NonNull TemplateDetailsItem newItem) {
+ if (oldItem.getType()
+ .equals(TemplateDetailsItem.Type.HEADER))
+ {
+ TemplateDetailsItem.Header oldHeader = oldItem.asHeaderItem();
+ TemplateDetailsItem.Header newHeader = newItem.asHeaderItem();
+
+ return oldHeader.equalContents(newHeader);
+ }
+ else {
+ TemplateDetailsItem.AccountRow oldAcc = oldItem.asAccountRowItem();
+ TemplateDetailsItem.AccountRow newAcc = newItem.asAccountRowItem();
+
+ return oldAcc.equalContents(newAcc);
+ }
+ }
+ });
+ itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.Callback() {
+ @Override
+ public float getMoveThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
+ return 0.5f;
+ }
+ @Override
+ public boolean isLongPressDragEnabled() {
+ return false;
+ }
+ @Override
+ public RecyclerView.ViewHolder chooseDropTarget(
+ @NonNull RecyclerView.ViewHolder selected,
+ @NonNull List<RecyclerView.ViewHolder> dropTargets, int curX, int curY) {
+ RecyclerView.ViewHolder best = null;
+ int bestDistance = 0;
+ for (RecyclerView.ViewHolder v : dropTargets) {
+ if (v == selected)
+ continue;
+
+ final int viewTop = v.itemView.getTop();
+ int distance = Math.abs(viewTop - curY);
+ if (best == null) {
+ best = v;
+ bestDistance = distance;
+ }
+ else {
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ best = v;
+ }
+ }
+ }
+
+ Logger.debug("dnd", "Best target is " + best);
+ return best;
+ }
+ @Override
+ public boolean canDropOver(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder current,
+ @NonNull RecyclerView.ViewHolder target) {
+ final int adapterPosition = target.getBindingAdapterPosition();
+
+ // first item is immovable
+ if (adapterPosition == 0)
+ return false;
+
+ return super.canDropOver(recyclerView, current, target);
+ }
+ @Override
+ public int getMovementFlags(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder) {
+ int flags = 0;
+ // the top item (transaction params) is always there
+ final int adapterPosition = viewHolder.getBindingAdapterPosition();
+ if (adapterPosition > 0)
+ flags |= makeFlag(ItemTouchHelper.ACTION_STATE_DRAG,
+ ItemTouchHelper.UP | ItemTouchHelper.DOWN) |
+ makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE,
+ ItemTouchHelper.START | ItemTouchHelper.END);
+
+ return flags;
+ }
+ @Override
+ public boolean onMove(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder,
+ @NonNull RecyclerView.ViewHolder target) {
+
+ final int fromPosition = viewHolder.getBindingAdapterPosition();
+ final int toPosition = target.getBindingAdapterPosition();
+ if (fromPosition == toPosition) {
+ Logger.debug("drag", String.format(Locale.US,
+ "Ignoring request to move an account from position %d to %d",
+ fromPosition, toPosition));
+ return false;
+ }
+
+ Logger.debug("drag",
+ String.format(Locale.US, "Moving account from %d to %d", fromPosition,
+ toPosition));
+ mModel.moveItem(fromPosition, toPosition);
+
+ return true;
+ }
+ @Override
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+ int pos = viewHolder.getBindingAdapterPosition();
+ mModel.removeItem(pos);
+ }
+ });
+ }
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onAttachedToRecyclerView(recyclerView);
+
+ itemTouchHelper.attachToRecyclerView(recyclerView);
+ }
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ super.onDetachedFromRecyclerView(recyclerView);
+
+ itemTouchHelper.attachToRecyclerView(null);
+ }
+ @Override
+ public long getItemId(int position) {
+ // header item is always first and IDs id may duplicate some of the account IDs
+ if (position == 0)
+ return 0;
+ TemplateDetailsItem.AccountRow accRow = differ.getCurrentList()
+ .get(position)
+ .asAccountRowItem();
+ return accRow.getId();
+ }
+ @Override
+ public int getItemViewType(int position) {
+
+ return differ.getCurrentList()
+ .get(position)
+ .getType()
+ .toInt();
+ }
+ @NonNull
+ @Override
+ public TemplateDetailsAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
+ int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case TemplateDetailsItem.TYPE.header:
+ return new Header(TemplateDetailsHeaderBinding.inflate(inflater, parent, false));
+ case TemplateDetailsItem.TYPE.accountItem:
+ return new AccountRow(
+ TemplateDetailsAccountBinding.inflate(inflater, parent, false));
+ default:
+ throw new IllegalStateException("Unsupported view type " + viewType);
+ }
+ }
+ @Override
+ public void onBindViewHolder(@NonNull TemplateDetailsAdapter.ViewHolder holder, int position) {
+ TemplateDetailsItem item = differ.getCurrentList()
+ .get(position);
+ holder.bind(item);
+ }
+ @Override
+ public int getItemCount() {
+ return differ.getCurrentList()
+ .size();
+ }
+ public void setItems(List<TemplateDetailsItem> items) {
+ if (BuildConfig.DEBUG) {
+ Logger.debug("tmpl", "Got new list");
+ for (int i = 1; i < items.size(); i++) {
+ final TemplateDetailsItem item = items.get(i);
+ Logger.debug("tmpl",
+ String.format(Locale.US, " %d: id %d, pos %d", i, item.getId(),
+ item.getPosition()));
+ }
+ }
+ differ.submitList(items);
+ }
+ public String getMatchGroupText(int groupNumber) {
+ TemplateDetailsItem.Header header = getHeader();
+ Pattern p = header.getCompiledPattern();
+ if (p == null)
+ return null;
+
+ final String testText = Misc.nullIsEmpty(header.getTestText());
+ Matcher m = p.matcher(testText);
+ if (m.matches() && m.groupCount() >= groupNumber)
+ return m.group(groupNumber);
+ else
+ return null;
+ }
+ protected TemplateDetailsItem.Header getHeader() {
+ return differ.getCurrentList()
+ .get(0)
+ .asHeaderItem();
+ }
+
+ private enum HeaderDetail {DESCRIPTION, COMMENT, DATE_YEAR, DATE_MONTH, DATE_DAY}
+
+ private enum AccDetail {ACCOUNT, COMMENT, AMOUNT, CURRENCY}
+
+ public abstract static class ViewHolder extends RecyclerView.ViewHolder {
+ ViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ abstract void bind(TemplateDetailsItem item);
+ }
+
+ private abstract static class BaseItem extends ViewHolder {
+ boolean updatePropagationDisabled = false;
+ BaseItem(@NonNull View itemView) {
+ super(itemView);
+ }
+ void disableUpdatePropagation() {
+ updatePropagationDisabled = true;
+ }
+ void enableUpdatePropagation() {
+ updatePropagationDisabled = false;
+ }
+ }
+
+ public class Header extends BaseItem {
+ private final TemplateDetailsHeaderBinding b;
+ boolean updatePropagationDisabled = false;
+ public Header(@NonNull TemplateDetailsHeaderBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+
+ TextWatcher templateNameWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ final TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed template name " + s + "; header=" + header);
+ header.setName(String.valueOf(s));
+ }
+ };
+ b.templateName.addTextChangedListener(templateNameWatcher);
+
+ TextWatcher patternWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ final TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed pattern " + s + "; header=" + header);
+ header.setPattern(String.valueOf(s));
+
+ checkPatternError(header);
+ }
+ };
+ b.pattern.addTextChangedListener(patternWatcher);
+
+ TextWatcher testTextWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ final TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed test text " + s + "; header=" + header);
+ header.setTestText(String.valueOf(s));
+
+ checkPatternError(header);
+ }
+ };
+ b.testText.addTextChangedListener(testTextWatcher);
+
+ TextWatcher transactionDescriptionWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ final TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed transaction description " + s + "; header=" + header);
+ header.setTransactionDescription(String.valueOf(s));
+ }
+ };
+ b.transactionDescription.addTextChangedListener(transactionDescriptionWatcher);
+ TextWatcher transactionCommentWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ final TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed transaction description " + s + "; header=" + header);
+ header.setTransactionComment(String.valueOf(s));
+ }
+ };
+ b.transactionComment.addTextChangedListener(transactionCommentWatcher);
+
+ b.templateIsFallbackSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (updatePropagationDisabled)
+ return;
+
+ getItem().setFallback(isChecked);
+ b.templateIsFallbackText.setText(isChecked ? R.string.template_is_fallback_yes
+ : R.string.template_is_fallback_no);
+ });
+ final View.OnClickListener fallbackLabelClickListener =
+ (view) -> b.templateIsFallbackSwitch.toggle();
+ b.templateIsFallbackLabel.setOnClickListener(fallbackLabelClickListener);
+ b.templateIsFallbackText.setOnClickListener(fallbackLabelClickListener);
+ b.templateParamsHelpButton.setOnClickListener(v -> HelpDialog.show(b.getRoot()
+ .getContext(),
+ R.string.template_details_template_params_label, R.array.template_params_help));
+ }
+ @NotNull
+ private TemplateDetailsItem.Header getItem() {
+ int pos = getBindingAdapterPosition();
+ return differ.getCurrentList()
+ .get(pos)
+ .asHeaderItem();
+ }
+ private void selectHeaderDetailSource(View v, HeaderDetail detail) {
+ TemplateDetailsItem.Header header = getItem();
+ Logger.debug(D_TEMPLATE_UI, "header is " + header);
+ TemplateDetailSourceSelectorFragment sel =
+ TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+ header.getTestText());
+ sel.setOnSourceSelectedListener((literal, group) -> {
+ if (literal) {
+ switch (detail) {
+ case DESCRIPTION:
+ header.switchToLiteralTransactionDescription();
+ break;
+ case COMMENT:
+ header.switchToLiteralTransactionComment();
+ break;
+ case DATE_YEAR:
+ header.switchToLiteralDateYear();
+ break;
+ case DATE_MONTH:
+ header.switchToLiteralDateMonth();
+ break;
+ case DATE_DAY:
+ header.switchToLiteralDateDay();
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+ else {
+ switch (detail) {
+ case DESCRIPTION:
+ header.setTransactionDescriptionMatchGroup(group);
+ break;
+ case COMMENT:
+ header.setTransactionCommentMatchGroup(group);
+ break;
+ case DATE_YEAR:
+ header.setDateYearMatchGroup(group);
+ break;
+ case DATE_MONTH:
+ header.setDateMonthMatchGroup(group);
+ break;
+ case DATE_DAY:
+ header.setDateDayMatchGroup(group);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+
+ notifyItemChanged(getBindingAdapterPosition());
+ });
+ final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
+ }
+ @Override
+ void bind(TemplateDetailsItem item) {
+ TemplateDetailsItem.Header header = item.asHeaderItem();
+ Logger.debug(D_TEMPLATE_UI, "Binding to header " + header);
+
+ disableUpdatePropagation();
+ try {
+ String groupNoText = b.getRoot()
+ .getResources()
+ .getString(R.string.template_item_match_group_source);
+
+ b.templateName.setText(header.getName());
+ b.pattern.setText(header.getPattern());
+ b.testText.setText(header.getTestText());
+
+ if (header.hasLiteralDateYear()) {
+ b.yearSource.setText(R.string.template_details_source_literal);
+ final Integer dateYear = header.getDateYear();
+ b.templateDetailsDateYear.setText(
+ (dateYear == null) ? null : String.valueOf(dateYear));
+ b.yearLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.yearLayout.setVisibility(View.GONE);
+ b.yearSource.setText(
+ String.format(Locale.US, groupNoText, header.getDateYearMatchGroup(),
+ getMatchGroupText(header.getDateYearMatchGroup())));
+ }
+ b.yearSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+ b.yearSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_YEAR));
+
+ if (header.hasLiteralDateMonth()) {
+ b.monthSource.setText(R.string.template_details_source_literal);
+ final Integer dateMonth = header.getDateMonth();
+ b.templateDetailsDateMonth.setText(
+ (dateMonth == null) ? null : String.valueOf(dateMonth));
+ b.monthLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.monthLayout.setVisibility(View.GONE);
+ b.monthSource.setText(
+ String.format(Locale.US, groupNoText, header.getDateMonthMatchGroup(),
+ getMatchGroupText(header.getDateMonthMatchGroup())));
+ }
+ b.monthSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+ b.monthSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_MONTH));
+
+ if (header.hasLiteralDateDay()) {
+ b.daySource.setText(R.string.template_details_source_literal);
+ final Integer dateDay = header.getDateDay();
+ b.templateDetailsDateDay.setText(
+ (dateDay == null) ? null : String.valueOf(dateDay));
+ b.dayLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.dayLayout.setVisibility(View.GONE);
+ b.daySource.setText(
+ String.format(Locale.US, groupNoText, header.getDateDayMatchGroup(),
+ getMatchGroupText(header.getDateDayMatchGroup())));
+ }
+ b.daySourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+ b.daySource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DATE_DAY));
+
+ if (header.hasLiteralTransactionDescription()) {
+ b.templateTransactionDescriptionSource.setText(
+ R.string.template_details_source_literal);
+ b.transactionDescription.setText(header.getTransactionDescription());
+ b.transactionDescriptionLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.transactionDescriptionLayout.setVisibility(View.GONE);
+ b.templateTransactionDescriptionSource.setText(
+ String.format(Locale.US, groupNoText,
+ header.getTransactionDescriptionMatchGroup(), getMatchGroupText(
+ header.getTransactionDescriptionMatchGroup())));
+
+ }
+ b.templateTransactionDescriptionSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+ b.templateTransactionDescriptionSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.DESCRIPTION));
+
+ if (header.hasLiteralTransactionComment()) {
+ b.templateTransactionCommentSource.setText(
+ R.string.template_details_source_literal);
+ b.transactionComment.setText(header.getTransactionComment());
+ b.transactionCommentLayout.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.transactionCommentLayout.setVisibility(View.GONE);
+ b.templateTransactionCommentSource.setText(String.format(Locale.US, groupNoText,
+ header.getTransactionCommentMatchGroup(),
+ getMatchGroupText(header.getTransactionCommentMatchGroup())));
+
+ }
+ b.templateTransactionCommentSourceLabel.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+ b.templateTransactionCommentSource.setOnClickListener(
+ v -> selectHeaderDetailSource(v, HeaderDetail.COMMENT));
+
+ b.templateDetailsHeadScanQrButton.setOnClickListener(this::scanTestQR);
+
+ b.templateIsFallbackSwitch.setChecked(header.isFallback());
+ b.templateIsFallbackText.setText(
+ header.isFallback() ? R.string.template_is_fallback_yes
+ : R.string.template_is_fallback_no);
+
+ checkPatternError(header);
+ }
+ finally {
+ enableUpdatePropagation();
+ }
+ }
+ private void checkPatternError(TemplateDetailsItem.Header item) {
+ if (item.getPatternError() != null) {
+ b.patternLayout.setError(item.getPatternError());
+ b.patternHintTitle.setVisibility(View.GONE);
+ b.patternHintText.setVisibility(View.GONE);
+ }
+ else {
+ b.patternLayout.setError(null);
+ if (item.testMatch() != null) {
+ b.patternHintText.setText(item.testMatch());
+ b.patternHintTitle.setVisibility(View.VISIBLE);
+ b.patternHintText.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.patternLayout.setError(null);
+ b.patternHintTitle.setVisibility(View.GONE);
+ b.patternHintText.setVisibility(View.GONE);
+ }
+ }
+
+ }
+ private void scanTestQR(View view) {
+ Context ctx = view.getContext();
+ if (ctx instanceof QR.QRScanTrigger)
+ ((QR.QRScanTrigger) ctx).triggerQRScan();
+ }
+ }
+
+ public class AccountRow extends BaseItem {
+ private final TemplateDetailsAccountBinding b;
+ public AccountRow(@NonNull TemplateDetailsAccountBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+
+ TextWatcher accountNameWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ TemplateDetailsItem.AccountRow accRow = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed account name " + s + "; accRow=" + accRow);
+ accRow.setAccountName(String.valueOf(s));
+
+ mModel.applyList(null);
+ }
+ };
+ b.templateDetailsAccountName.addTextChangedListener(accountNameWatcher);
+ b.templateDetailsAccountName.setAdapter(new AccountAutocompleteAdapter(b.getRoot()
+ .getContext()));
+ b.templateDetailsAccountName.setOnItemClickListener(
+ (parent, view, position, id) -> b.templateDetailsAccountName.setText(
+ ((TextView) view).getText()));
+ TextWatcher accountCommentWatcher = new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ TemplateDetailsItem.AccountRow accRow = getItem();
+ Logger.debug(D_TEMPLATE_UI,
+ "Storing changed account comment " + s + "; accRow=" + accRow);
+ accRow.setAccountComment(String.valueOf(s));
+
+ mModel.applyList(null);
+ }
+ };
+ b.templateDetailsAccountComment.addTextChangedListener(accountCommentWatcher);
+
+ b.templateDetailsAccountAmount.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (updatePropagationDisabled)
+ return;
+
+ TemplateDetailsItem.AccountRow accRow = getItem();
+
+ String str = String.valueOf(s);
+ if (Misc.emptyIsNull(str) == null) {
+ accRow.setAmount(null);
+ }
+ else {
+ try {
+ final float amount = Data.parseNumber(str);
+ accRow.setAmount(amount);
+ b.templateDetailsAccountAmountLayout.setError(null);
+
+ Logger.debug(D_TEMPLATE_UI, String.format(Locale.US,
+ "Storing changed account amount %s [%4.2f]; accRow=%s", s,
+ amount, accRow));
+ }
+ catch (NumberFormatException | ParseException e) {
+ b.templateDetailsAccountAmountLayout.setError("!");
+ }
+ }
+
+ mModel.applyList(null);
+ }
+ });
+ b.templateDetailsAccountAmount.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus)
+ return;
+
+ TemplateDetailsItem.AccountRow accRow = getItem();
+ if (!accRow.hasLiteralAmount())
+ return;
+ Float amt = accRow.getAmount();
+ if (amt == null)
+ return;
+
+ b.templateDetailsAccountAmount.setText(Data.formatNumber(amt));
+ });
+
+ b.negateAmountSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (updatePropagationDisabled)
+ return;
+
+ getItem().setNegateAmount(isChecked);
+ b.templateDetailsNegateAmountText.setText(
+ isChecked ? R.string.template_account_change_amount_sign
+ : R.string.template_account_keep_amount_sign);
+ });
+ final View.OnClickListener negLabelClickListener =
+ (view) -> b.negateAmountSwitch.toggle();
+ b.templateDetailsNegateAmountLabel.setOnClickListener(negLabelClickListener);
+ b.templateDetailsNegateAmountText.setOnClickListener(negLabelClickListener);
+ manageAccountLabelDrag();
+ }
+ @SuppressLint("ClickableViewAccessibility")
+ public void manageAccountLabelDrag() {
+ b.patternAccountLabel.setOnTouchListener((v, event) -> {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ itemTouchHelper.startDrag(this);
+ }
+ return false;
+ });
+ }
+ @Override
+ void bind(TemplateDetailsItem item) {
+ disableUpdatePropagation();
+ try {
+ final Resources resources = b.getRoot()
+ .getResources();
+ String groupNoText = resources.getString(R.string.template_item_match_group_source);
+
+ Logger.debug("drag", String.format(Locale.US, "Binding account id %d, pos %d at %d",
+ item.getId(), item.getPosition(), getBindingAdapterPosition()));
+ TemplateDetailsItem.AccountRow accRow = item.asAccountRowItem();
+ b.patternAccountLabel.setText(String.format(Locale.US,
+ resources.getString(R.string.template_details_account_row_label),
+ accRow.getPosition()));
+ if (accRow.hasLiteralAccountName()) {
+ b.templateDetailsAccountNameLayout.setVisibility(View.VISIBLE);
+ b.templateDetailsAccountName.setText(accRow.getAccountName());
+ b.templateDetailsAccountNameSource.setText(
+ R.string.template_details_source_literal);
+ }
+ else {
+ b.templateDetailsAccountNameLayout.setVisibility(View.GONE);
+ b.templateDetailsAccountNameSource.setText(
+ String.format(Locale.US, groupNoText, accRow.getAccountNameMatchGroup(),
+ getMatchGroupText(accRow.getAccountNameMatchGroup())));
+ }
+
+ if (accRow.hasLiteralAccountComment()) {
+ b.templateDetailsAccountCommentLayout.setVisibility(View.VISIBLE);
+ b.templateDetailsAccountComment.setText(accRow.getAccountComment());
+ b.templateDetailsAccountCommentSource.setText(
+ R.string.template_details_source_literal);
+ }
+ else {
+ b.templateDetailsAccountCommentLayout.setVisibility(View.GONE);
+ b.templateDetailsAccountCommentSource.setText(
+ String.format(Locale.US, groupNoText,
+ accRow.getAccountCommentMatchGroup(),
+ getMatchGroupText(accRow.getAccountCommentMatchGroup())));
+ }
+
+ if (accRow.hasLiteralAmount()) {
+ b.templateDetailsAccountAmountSource.setText(
+ R.string.template_details_source_literal);
+ b.templateDetailsAccountAmount.setVisibility(View.VISIBLE);
+ Float amt = accRow.getAmount();
+ b.templateDetailsAccountAmount.setText((amt == null) ? null : String.format(
+ Data.locale.getValue(), "%,4.2f", (accRow.getAmount())));
+ b.negateAmountSwitch.setVisibility(View.GONE);
+ b.templateDetailsNegateAmountLabel.setVisibility(View.GONE);
+ b.templateDetailsNegateAmountText.setVisibility(View.GONE);
+ }
+ else {
+ b.templateDetailsAccountAmountSource.setText(
+ String.format(Locale.US, groupNoText, accRow.getAmountMatchGroup(),
+ getMatchGroupText(accRow.getAmountMatchGroup())));
+ b.templateDetailsAccountAmountLayout.setVisibility(View.GONE);
+ b.negateAmountSwitch.setVisibility(View.VISIBLE);
+ b.negateAmountSwitch.setChecked(accRow.isNegateAmount());
+ b.templateDetailsNegateAmountText.setText(
+ accRow.isNegateAmount() ? R.string.template_account_change_amount_sign
+ : R.string.template_account_keep_amount_sign);
+ b.templateDetailsNegateAmountLabel.setVisibility(View.VISIBLE);
+ b.templateDetailsNegateAmountText.setVisibility(View.VISIBLE);
+ }
+
+ if (accRow.hasLiteralCurrency()) {
+ b.templateDetailsAccountCurrencySource.setText(
+ R.string.template_details_source_literal);
+ net.ktnx.mobileledger.db.Currency c = accRow.getCurrency();
+ if (c == null)
+ b.templateDetailsAccountCurrency.setText(R.string.btn_no_currency);
+ else
+ b.templateDetailsAccountCurrency.setText(c.getName());
+ b.templateDetailsAccountCurrency.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.templateDetailsAccountCurrencySource.setText(
+ String.format(Locale.US, groupNoText, accRow.getCurrencyMatchGroup(),
+ getMatchGroupText(accRow.getCurrencyMatchGroup())));
+ b.templateDetailsAccountCurrency.setVisibility(View.GONE);
+ }
+
+ b.templateAccountNameSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+ b.templateDetailsAccountNameSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.ACCOUNT));
+ b.templateAccountCommentSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+ b.templateDetailsAccountCommentSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.COMMENT));
+ b.templateAccountAmountSourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
+ b.templateDetailsAccountAmountSource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.AMOUNT));
+ b.templateDetailsAccountCurrencySource.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.CURRENCY));
+ b.templateAccountCurrencySourceLabel.setOnClickListener(
+ v -> selectAccountRowDetailSource(v, AccDetail.CURRENCY));
+ if (accRow.hasLiteralCurrency())
+ b.templateDetailsAccountCurrency.setOnClickListener(v -> {
+ CurrencySelectorFragment cpf = CurrencySelectorFragment.newInstance(
+ CurrencySelectorFragment.DEFAULT_COLUMN_COUNT, false);
+ cpf.setOnCurrencySelectedListener(text -> {
+ if (text == null) {
+ b.templateDetailsAccountCurrency.setText(R.string.btn_no_currency);
+ accRow.setCurrency(null);
+ }
+ else {
+ b.templateDetailsAccountCurrency.setText(text);
+ DB.get()
+ .getCurrencyDAO()
+ .getByName(text)
+ .observe((LifecycleOwner) b.getRoot()
+ .getContext(), accRow::setCurrency);
+ }
+ });
+ cpf.show(
+ ((TemplatesActivity) b.templateDetailsAccountCurrency.getContext()).getSupportFragmentManager(),
+ "currency-selector");
+ });
+ }
+ finally {
+ enableUpdatePropagation();
+ }
+ }
+ private @NotNull TemplateDetailsItem.AccountRow getItem() {
+ return differ.getCurrentList()
+ .get(getBindingAdapterPosition())
+ .asAccountRowItem();
+ }
+ private void selectAccountRowDetailSource(View v, AccDetail detail) {
+ TemplateDetailsItem.AccountRow accRow = getItem();
+ final TemplateDetailsItem.Header header = getHeader();
+ Logger.debug(D_TEMPLATE_UI, "header is " + header);
+ TemplateDetailSourceSelectorFragment sel =
+ TemplateDetailSourceSelectorFragment.newInstance(1, header.getPattern(),
+ header.getTestText());
+ sel.setOnSourceSelectedListener((literal, group) -> {
+ if (literal) {
+ switch (detail) {
+ case ACCOUNT:
+ accRow.switchToLiteralAccountName();
+ break;
+ case COMMENT:
+ accRow.switchToLiteralAccountComment();
+ break;
+ case AMOUNT:
+ accRow.switchToLiteralAmount();
+ break;
+ case CURRENCY:
+ accRow.switchToLiteralCurrency();
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+ else {
+ switch (detail) {
+ case ACCOUNT:
+ accRow.setAccountNameMatchGroup(group);
+ break;
+ case COMMENT:
+ accRow.setAccountCommentMatchGroup(group);
+ break;
+ case AMOUNT:
+ accRow.setAmountMatchGroup(group);
+ break;
+ case CURRENCY:
+ accRow.setCurrencyMatchGroup(group);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected detail " + detail);
+ }
+ }
+
+ notifyItemChanged(getBindingAdapterPosition());
+ });
+ final AppCompatActivity activity = (AppCompatActivity) v.getContext();
+ sel.show(activity.getSupportFragmentManager(), "template-details-source-selector");
+ }
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
+import androidx.navigation.NavController;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.TemplateDetailsFragmentBinding;
+import net.ktnx.mobileledger.ui.FabManager;
+import net.ktnx.mobileledger.utils.Logger;
+
+public class TemplateDetailsFragment extends Fragment {
+ static final String ARG_TEMPLATE_ID = "pattern-id";
+ private static final String ARG_COLUMN_COUNT = "column-count";
+ private TemplateDetailsFragmentBinding b;
+ private TemplateDetailsViewModel mViewModel;
+ private int mColumnCount = 1;
+ private Long mPatternId;
+ private InteractionListener interactionListener;
+ public TemplateDetailsFragment() {
+ }
+ public static TemplateDetailsFragment newInstance(int columnCount, int patternId) {
+ final TemplateDetailsFragment fragment = new TemplateDetailsFragment();
+ Bundle args = new Bundle();
+ args.putInt(ARG_COLUMN_COUNT, columnCount);
+ if (patternId > 0)
+ args.putInt(ARG_TEMPLATE_ID, patternId);
+ fragment.setArguments(args);
+ return fragment;
+ }
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.template_details_menu, menu);
+ }
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.delete_template) {
+ signalDeleteTemplateInteraction();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+ private void signalDeleteTemplateInteraction() {
+ if (interactionListener != null)
+ interactionListener.onDeleteTemplate(mPatternId);
+ }
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Bundle args = getArguments();
+ if (args != null) {
+ mColumnCount = args.getInt(ARG_COLUMN_COUNT, 1);
+ mPatternId = args.getLong(ARG_TEMPLATE_ID, -1);
+ if (mPatternId == -1)
+ mPatternId = null;
+ }
+
+ setHasOptionsMenu(mPatternId != null);
+ }
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ if (!(getActivity() instanceof InteractionListener))
+ throw new IllegalStateException(
+ "Containing activity must implement TemplateDetailsFragment" +
+ ".InteractionListener");
+ interactionListener = (InteractionListener) getActivity();
+
+ NavController controller = ((TemplatesActivity) requireActivity()).getNavController();
+ final ViewModelStoreOwner viewModelStoreOwner =
+ controller.getViewModelStoreOwner(R.id.template_list_navigation);
+ mViewModel = new ViewModelProvider(viewModelStoreOwner).get(TemplateDetailsViewModel.class);
+ mViewModel.setDefaultTemplateName(getString(R.string.unnamed_template));
+ Logger.debug("flow", "PatternDetailsFragment.onCreateView(): model=" + mViewModel);
+
+ b = TemplateDetailsFragmentBinding.inflate(inflater);
+ Context context = b.patternDetailsRecyclerView.getContext();
+ if (mColumnCount <= 1) {
+ b.patternDetailsRecyclerView.setLayoutManager(new LinearLayoutManager(context));
+ }
+ else {
+ b.patternDetailsRecyclerView.setLayoutManager(
+ new GridLayoutManager(context, mColumnCount));
+ }
+
+
+ TemplateDetailsAdapter adapter = new TemplateDetailsAdapter(mViewModel);
+ b.patternDetailsRecyclerView.setAdapter(adapter);
+ mViewModel.getItems(mPatternId)
+ .observe(getViewLifecycleOwner(), adapter::setItems);
+
+ FragmentActivity activity = requireActivity();
+ if (activity instanceof FabManager.FabHandler)
+ FabManager.handle((FabManager.FabHandler) activity, b.patternDetailsRecyclerView);
+
+ return b.getRoot();
+ }
+ interface InteractionListener {
+ void onDeleteTemplate(@NonNull Long templateId);
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.BuildConfig;
+import net.ktnx.mobileledger.dao.BaseDAO;
+import net.ktnx.mobileledger.dao.TemplateAccountDAO;
+import net.ktnx.mobileledger.dao.TemplateHeaderDAO;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateAccount;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.db.TemplateWithAccounts;
+import net.ktnx.mobileledger.model.TemplateDetailsItem;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class TemplateDetailsViewModel extends ViewModel {
+ static final String TAG = "template-details-model";
+ static final String DB_TAG = TAG + "-db";
+ private final MutableLiveData<List<TemplateDetailsItem>> items =
+ new MutableLiveData<>(Collections.emptyList());
+ private final AtomicInteger syntheticItemId = new AtomicInteger(0);
+ private Long mPatternId;
+ private String mDefaultTemplateName;
+ private boolean itemsLoaded = false;
+ public String getDefaultTemplateName() {
+ return mDefaultTemplateName;
+ }
+ public void setDefaultTemplateName(String name) {
+ mDefaultTemplateName = name;
+ }
+
+ public void resetItems() {
+ applyList(new ArrayList<>());
+ }
+ public void applyList(List<TemplateDetailsItem> srcList) {
+ applyList(srcList, false);
+ }
+ public void applyList(List<TemplateDetailsItem> srcList, boolean async) {
+ boolean changes;
+ if (srcList == null) {
+ srcList = new ArrayList<>(items.getValue());
+ changes = false;
+ }
+ else
+ changes = true;
+
+ srcList = Collections.unmodifiableList(srcList);
+
+ if (BuildConfig.DEBUG) {
+ Logger.debug(TAG, "Considering old list");
+ for (TemplateDetailsItem item : srcList)
+ Logger.debug(TAG, String.format(Locale.US, " id %d pos %d", item.getId(),
+ item.getPosition()));
+ }
+
+ ArrayList<TemplateDetailsItem> newList = new ArrayList<>();
+
+ boolean hasEmptyItem = false;
+
+ if (srcList.size() < 1) {
+ final TemplateDetailsItem.Header header = TemplateDetailsItem.createHeader();
+ header.setId(0);
+ newList.add(header);
+ changes = true;
+ }
+ else {
+ newList.add(srcList.get(0));
+ }
+
+ for (int i = 1; i < srcList.size(); i++) {
+ final TemplateDetailsItem.AccountRow accRow = srcList.get(i)
+ .asAccountRowItem();
+ if (accRow.isEmpty()) {
+ // it is normal to have two empty rows if they are at the
+ // top (position 1 and 2)
+ if (!hasEmptyItem || i < 3) {
+ accRow.setPosition(newList.size());
+ newList.add(accRow);
+ }
+ else
+ changes = true; // row skipped
+
+ hasEmptyItem = true;
+ }
+ else {
+ accRow.setPosition(newList.size());
+ newList.add(accRow);
+ }
+ }
+
+ while (newList.size() < 3) {
+ final TemplateDetailsItem.AccountRow accountRow =
+ TemplateDetailsItem.createAccountRow();
+ accountRow.setId(genItemId());
+ accountRow.setPosition(newList.size());
+ newList.add(accountRow);
+ changes = true;
+ hasEmptyItem = true;
+ }
+
+ if (!hasEmptyItem) {
+ final TemplateDetailsItem.AccountRow accountRow =
+ TemplateDetailsItem.createAccountRow();
+ accountRow.setId(genItemId());
+ accountRow.setPosition(newList.size());
+ newList.add(accountRow);
+ changes = true;
+ }
+
+ if (changes) {
+ Logger.debug(TAG, "Changes detected, applying new list");
+
+ if (async)
+ items.postValue(newList);
+ else
+ items.setValue(newList);
+ }
+ else
+ Logger.debug(TAG, "No changes, ignoring new list");
+ }
+ public int genItemId() {
+ return syntheticItemId.decrementAndGet();
+ }
+ public LiveData<List<TemplateDetailsItem>> getItems(Long patternId) {
+ if (itemsLoaded && Objects.equals(patternId, this.mPatternId))
+ return items;
+
+ if (patternId != null && patternId <= 0)
+ throw new IllegalArgumentException("Pattern ID " + patternId + " is invalid");
+
+ mPatternId = patternId;
+
+ if (mPatternId == null) {
+ resetItems();
+ itemsLoaded = true;
+ return items;
+ }
+
+ DB db = DB.get();
+ LiveData<TemplateWithAccounts> dbList = db.getTemplateDAO()
+ .getTemplateWithAccounts(mPatternId);
+ Observer<TemplateWithAccounts> observer = new Observer<TemplateWithAccounts>() {
+ @Override
+ public void onChanged(TemplateWithAccounts src) {
+ ArrayList<TemplateDetailsItem> l = new ArrayList<>();
+
+ TemplateDetailsItem header = TemplateDetailsItem.fromRoomObject(src.header);
+ Logger.debug(DB_TAG, "Got header template item with id of " + header.getId());
+ l.add(header);
+ Collections.sort(src.accounts,
+ (o1, o2) -> Long.compare(o1.getPosition(), o2.getPosition()));
+ for (TemplateAccount acc : src.accounts) {
+ l.add(TemplateDetailsItem.fromRoomObject(acc));
+ }
+
+ for (TemplateDetailsItem i : l) {
+ Logger.debug(DB_TAG, "Loaded pattern item " + i);
+ }
+ applyList(l, true);
+ itemsLoaded = true;
+
+ dbList.removeObserver(this);
+ }
+ };
+ dbList.observeForever(observer);
+
+ return items;
+ }
+ public void setTestText(String text) {
+ List<TemplateDetailsItem> list = new ArrayList<>(items.getValue());
+ TemplateDetailsItem.Header header = new TemplateDetailsItem.Header(list.get(0)
+ .asHeaderItem());
+ header.setTestText(text);
+ list.set(0, header);
+
+ items.setValue(list);
+ }
+ public void onSaveTemplate() {
+ Logger.debug("flow", "PatternDetailsViewModel.onSavePattern(); model=" + this);
+ final List<TemplateDetailsItem> list = Objects.requireNonNull(items.getValue());
+
+ BaseDAO.runAsync(() -> {
+ boolean newPattern = mPatternId == null || mPatternId <= 0;
+
+ TemplateDetailsItem.Header modelHeader = list.get(0)
+ .asHeaderItem();
+
+ modelHeader.setName(Misc.trim(modelHeader.getName()));
+ if (modelHeader.getName()
+ .isEmpty())
+ modelHeader.setName(getDefaultTemplateName());
+
+ TemplateHeaderDAO headerDAO = DB.get()
+ .getTemplateDAO();
+ TemplateHeader dbHeader = modelHeader.toDBO();
+ if (newPattern) {
+ dbHeader.setId(0L);
+ dbHeader.setId(mPatternId = headerDAO.insertSync(dbHeader));
+ }
+ else
+ headerDAO.updateSync(dbHeader);
+
+ Logger.debug("pattern-db",
+ String.format(Locale.US, "Stored pattern header %d, item=%s", dbHeader.getId(),
+ modelHeader));
+
+
+ TemplateAccountDAO taDAO = DB.get()
+ .getTemplateAccountDAO();
+ taDAO.prepareForSave(mPatternId);
+ for (int i = 1; i < list.size(); i++) {
+ final TemplateDetailsItem.AccountRow accRowItem = list.get(i)
+ .asAccountRowItem();
+ TemplateAccount dbAccount = accRowItem.toDBO(dbHeader.getId());
+ dbAccount.setTemplateId(mPatternId);
+ dbAccount.setPosition(i);
+ if (dbAccount.getId() < 0) {
+ dbAccount.setId(0);
+ dbAccount.setId(taDAO.insertSync(dbAccount));
+ }
+ else
+ taDAO.updateSync(dbAccount);
+
+ Logger.debug("pattern-db", String.format(Locale.US,
+ "Stored pattern account %d, account=%s, comment=%s, neg=%s, item=%s",
+ dbAccount.getId(), dbAccount.getAccountName(),
+ dbAccount.getAccountComment(), dbAccount.getNegateAmount(), accRowItem));
+ }
+ taDAO.finishSave(mPatternId);
+ });
+ }
+ private ArrayList<TemplateDetailsItem> copyItems() {
+ List<TemplateDetailsItem> oldList = items.getValue();
+
+ if (oldList == null)
+ return new ArrayList<>();
+
+ ArrayList<TemplateDetailsItem> result = new ArrayList<>(oldList.size());
+
+ for (TemplateDetailsItem item : oldList) {
+ if (item instanceof TemplateDetailsItem.Header)
+ result.add(new TemplateDetailsItem.Header(item.asHeaderItem()));
+ else if (item instanceof TemplateDetailsItem.AccountRow)
+ result.add(new TemplateDetailsItem.AccountRow(item.asAccountRowItem()));
+ else
+ throw new RuntimeException("Unexpected item " + item);
+ }
+
+ return result;
+ }
+ public void moveItem(int sourcePos, int targetPos) {
+ final List<TemplateDetailsItem> newList = copyItems();
+
+ if (BuildConfig.DEBUG) {
+ Logger.debug("drag", "Before move:");
+ for (int i = 1; i < newList.size(); i++) {
+ final TemplateDetailsItem item = newList.get(i);
+ Logger.debug("drag",
+ String.format(Locale.US, " %d: id %d, pos %d", i, item.getId(),
+ item.getPosition()));
+ }
+ }
+
+ {
+ TemplateDetailsItem item = newList.remove(sourcePos);
+ newList.add(targetPos, item);
+ }
+
+ // adjust affected items' positions
+ {
+ int startPos, endPos;
+ if (sourcePos < targetPos) {
+ // moved down
+ startPos = sourcePos;
+ endPos = targetPos;
+ }
+ else {
+ // moved up
+ startPos = targetPos;
+ endPos = sourcePos;
+ }
+
+ for (int i = startPos; i <= endPos; i++) {
+ newList.get(i)
+ .setPosition(i);
+ }
+ }
+
+ if (BuildConfig.DEBUG) {
+ Logger.debug("drag", "After move:");
+ for (int i = 1; i < newList.size(); i++) {
+ final TemplateDetailsItem item = newList.get(i);
+ Logger.debug("drag",
+ String.format(Locale.US, " %d: id %d, pos %d", i, item.getId(),
+ item.getPosition()));
+ }
+ }
+
+ items.setValue(newList);
+ }
+ public void removeItem(int position) {
+ Logger.debug(TAG, "Removing item at position " + position);
+ ArrayList<TemplateDetailsItem> newList = copyItems();
+ newList.remove(position);
+ for (int i = position; i < newList.size(); i++)
+ newList.get(i)
+ .setPosition(i);
+ applyList(newList);
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.Objects;
+
+class TemplateListDivider extends DividerItemDecoration {
+ private final Rect mBounds = new Rect();
+ private int mOrientation;
+ public TemplateListDivider(Context context, int orientation) {
+ super(context, orientation);
+ mOrientation = orientation;
+ }
+ @Override
+ public void setOrientation(int orientation) {
+ super.setOrientation(orientation);
+ mOrientation = orientation;
+ }
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ if (parent.getLayoutManager() == null || getDrawable() == null) {
+ return;
+ }
+ if (mOrientation == VERTICAL) {
+ drawVertical(c, parent);
+ }
+ else {
+ drawHorizontal(c, parent);
+ }
+ }
+
+ private void drawVertical(Canvas canvas, RecyclerView parent) {
+ canvas.save();
+ final int left;
+ final int right;
+ //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+ if (parent.getClipToPadding()) {
+ left = parent.getPaddingLeft();
+ right = parent.getWidth() - parent.getPaddingRight();
+ canvas.clipRect(left, parent.getPaddingTop(), right,
+ parent.getHeight() - parent.getPaddingBottom());
+ }
+ else {
+ left = 0;
+ right = parent.getWidth();
+ }
+
+ final Drawable divider = Objects.requireNonNull(getDrawable());
+ final int childCount = parent.getChildCount();
+ final TemplatesRecyclerViewAdapter adapter =
+ (TemplatesRecyclerViewAdapter) Objects.requireNonNull(parent.getAdapter());
+ final int itemCount = adapter.getItemCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final int childAdapterPosition = parent.getChildAdapterPosition(child);
+ if (childAdapterPosition == RecyclerView.NO_POSITION ||
+ adapter.getItemViewType(childAdapterPosition) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER ||
+ childAdapterPosition + 1 < itemCount &&
+ adapter.getItemViewType(childAdapterPosition + 1) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER)
+ continue;
+ parent.getDecoratedBoundsWithMargins(child, mBounds);
+ final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
+ final int top = bottom - divider.getIntrinsicHeight();
+ divider.setBounds(left, top, right, bottom);
+ divider.draw(canvas);
+ }
+ canvas.restore();
+ }
+
+ private void drawHorizontal(Canvas canvas, RecyclerView parent) {
+ final RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
+ if (layoutManager == null)
+ return;
+
+ canvas.save();
+ final int top;
+ final int bottom;
+ //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
+ if (parent.getClipToPadding()) {
+ top = parent.getPaddingTop();
+ bottom = parent.getHeight() - parent.getPaddingBottom();
+ canvas.clipRect(parent.getPaddingLeft(), top,
+ parent.getWidth() - parent.getPaddingRight(), bottom);
+ }
+ else {
+ top = 0;
+ bottom = parent.getHeight();
+ }
+
+ final Drawable divider = Objects.requireNonNull(getDrawable());
+ final int childCount = parent.getChildCount();
+ final TemplatesRecyclerViewAdapter adapter =
+ (TemplatesRecyclerViewAdapter) Objects.requireNonNull(parent.getAdapter());
+ final int itemCount = adapter.getItemCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = parent.getChildAt(i);
+ final int childAdapterPosition = parent.getChildAdapterPosition(child);
+ if (childAdapterPosition == RecyclerView.NO_POSITION ||
+ adapter.getItemViewType(childAdapterPosition) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER ||
+ childAdapterPosition + 1 < itemCount &&
+ adapter.getItemViewType(childAdapterPosition + 1) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER)
+ {
+ continue;
+ }
+ layoutManager.getDecoratedBoundsWithMargins(child, mBounds);
+ final int right = mBounds.right + Math.round(child.getTranslationX());
+ final int left = right - divider.getIntrinsicWidth();
+ divider.setBounds(left, top, right, bottom);
+ divider.draw(canvas);
+ }
+ canvas.restore();
+ }
+ @Override
+ public void getItemOffsets(Rect outRect, View child, RecyclerView parent,
+ RecyclerView.State state) {
+ final int childAdapterPosition = parent.getChildAdapterPosition(child);
+ final TemplatesRecyclerViewAdapter adapter =
+ (TemplatesRecyclerViewAdapter) Objects.requireNonNull(parent.getAdapter());
+ final int itemCount = adapter.getItemCount();
+
+ if (childAdapterPosition == RecyclerView.NO_POSITION ||
+ adapter.getItemViewType(childAdapterPosition) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER ||
+ childAdapterPosition + 1 < itemCount &&
+ adapter.getItemViewType(childAdapterPosition + 1) ==
+ TemplatesRecyclerViewAdapter.ITEM_TYPE_DIVIDER)
+ return;
+
+ super.getItemOffsets(outRect, child, parent, state);
+ }
+}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleEventObserver;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.DividerItemDecoration;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.dao.TemplateHeaderDAO;
+import net.ktnx.mobileledger.databinding.FragmentTemplateListBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateHeader;
+import net.ktnx.mobileledger.ui.FabManager;
+import net.ktnx.mobileledger.ui.HelpDialog;
+import net.ktnx.mobileledger.utils.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * A simple {@link Fragment} subclass.
+ * Use the {@link TemplateListFragment#newInstance} factory method to
+ * create an instance of this fragment.
+ */
+public class TemplateListFragment extends Fragment {
+ private FragmentTemplateListBinding b;
+ private OnTemplateListFragmentInteractionListener mListener;
+ public TemplateListFragment() {
+ // Required empty public constructor
+ }
+ /**
+ * Use this factory method to create a new instance of
+ * this fragment using the provided parameters.
+ *
+ * @return A new instance of fragment TemplateListFragment.
+ */
+ public static TemplateListFragment newInstance() {
+ TemplateListFragment fragment = new TemplateListFragment();
+ Bundle args = new Bundle();
+ fragment.setArguments(args);
+ return fragment;
+ }
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ inflater.inflate(R.menu.template_list_menu, menu);
+ }
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ if (item.getItemId() == R.id.menu_item_template_list_help) {
+ HelpDialog.show(requireContext(), R.string.template_list_help_title,
+ R.array.template_list_help_text);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(@NotNull LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Logger.debug("flow", "PatternListFragment.onCreateView()");
+ b = FragmentTemplateListBinding.inflate(inflater);
+ FragmentActivity activity = requireActivity();
+
+ if (activity instanceof FabManager.FabHandler)
+ FabManager.handle((FabManager.FabHandler) activity, b.templateList);
+
+ TemplatesRecyclerViewAdapter modelAdapter = new TemplatesRecyclerViewAdapter();
+
+ b.templateList.setAdapter(modelAdapter);
+ TemplateHeaderDAO pDao = DB.get()
+ .getTemplateDAO();
+ LiveData<List<TemplateHeader>> templates = pDao.getTemplates();
+ templates.observe(getViewLifecycleOwner(), modelAdapter::setTemplates);
+ LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ llm.setOrientation(RecyclerView.VERTICAL);
+ b.templateList.setLayoutManager(llm);
+ DividerItemDecoration did =
+ new TemplateListDivider(activity, DividerItemDecoration.VERTICAL);
+ b.templateList.addItemDecoration(did);
+
+ return b.getRoot();
+ }
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof OnTemplateListFragmentInteractionListener) {
+ mListener = (OnTemplateListFragmentInteractionListener) context;
+ }
+ else {
+ throw new RuntimeException(
+ context.toString() + " must implement OnFragmentInteractionListener");
+ }
+
+ final LifecycleEventObserver observer = new LifecycleEventObserver() {
+ @Override
+ public void onStateChanged(@NonNull LifecycleOwner source,
+ @NonNull Lifecycle.Event event) {
+ if (event.getTargetState() == Lifecycle.State.CREATED) {
+// getActivity().setActionBar(b.toolbar);
+ getLifecycle().removeObserver(this);
+ }
+ }
+ };
+ getLifecycle().addObserver(observer);
+ }
+ /**
+ * This interface must be implemented by activities that contain this
+ * fragment to allow an interaction in this fragment to be communicated
+ * to the activity and potentially other fragments contained in that
+ * activity.
+ * <p>
+ * See the Android Training lesson <a href=
+ * "http://developer.android.com/training/basics/fragments/communicating.html"
+ * >Communicating with Other Fragments</a> for more information.
+ */
+ public interface OnTemplateListFragmentInteractionListener {
+ void onSaveTemplate();
+
+ void onEditTemplate(Long id);
+
+ void onDuplicateTemplate(long id);
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.databinding.TemplateListTemplateItemBinding;
+import net.ktnx.mobileledger.databinding.TemplatesFallbackDividerBinding;
+import net.ktnx.mobileledger.db.TemplateHeader;
+
+abstract class BaseTemplateViewHolder extends RecyclerView.ViewHolder {
+ public BaseTemplateViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+ abstract void bindToItem(TemplatesRecyclerViewAdapter.BaseTemplateItem item);
+ static class TemplateDividerViewHolder extends BaseTemplateViewHolder {
+ public TemplateDividerViewHolder(@NonNull TemplatesFallbackDividerBinding binding) {
+ super(binding.getRoot());
+ }
+ @Override
+ void bindToItem(TemplatesRecyclerViewAdapter.BaseTemplateItem item) {
+ // nothing
+ }
+ }
+
+ static class TemplateViewHolder extends BaseTemplateViewHolder {
+ final TemplateListTemplateItemBinding b;
+ public TemplateViewHolder(@NonNull TemplateListTemplateItemBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ @Override
+ public void bindToItem(TemplatesRecyclerViewAdapter.BaseTemplateItem baseItem) {
+ TemplateHeader item = ((TemplatesRecyclerViewAdapter.TemplateItem) baseItem).template;
+ b.templateName.setText(item.getName());
+ b.templateName.setOnClickListener(
+ v -> ((TemplatesActivity) v.getContext()).onEditTemplate(item.getId()));
+ b.templateName.setOnLongClickListener((v) -> {
+ TemplatesActivity activity = (TemplatesActivity) v.getContext();
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ final String templateName = item.getName();
+ builder.setTitle(templateName);
+ builder.setItems(R.array.templates_ctx_menu, (dialog, which) -> {
+ if (which == 0) { // edit
+ activity.onEditTemplate(item.getId());
+ }
+ else if (which == 1) { // duplicate
+ activity.onDuplicateTemplate(item.getId());
+ }
+ else if (which == 2) { // delete
+ activity.onDeleteTemplate(item.getId());
+ }
+ else {
+ throw new RuntimeException(
+ String.format("Unknown menu item id (%d)", which));
+ }
+ dialog.dismiss();
+ });
+ builder.show();
+ return true;
+ });
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2022 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.ViewModelStoreOwner;
+import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
+import androidx.navigation.fragment.NavHostFragment;
+
+import com.google.android.material.snackbar.BaseTransientBottomBar;
+import com.google.android.material.snackbar.Snackbar;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.dao.TemplateHeaderDAO;
+import net.ktnx.mobileledger.databinding.ActivityTemplatesBinding;
+import net.ktnx.mobileledger.db.DB;
+import net.ktnx.mobileledger.db.TemplateWithAccounts;
+import net.ktnx.mobileledger.ui.FabManager;
+import net.ktnx.mobileledger.ui.QR;
+import net.ktnx.mobileledger.ui.activity.CrashReportingActivity;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.Objects;
+
+public class TemplatesActivity extends CrashReportingActivity
+ implements TemplateListFragment.OnTemplateListFragmentInteractionListener,
+ TemplateDetailsFragment.InteractionListener, QR.QRScanResultReceiver, QR.QRScanTrigger,
+ FabManager.FabHandler {
+ public static final String ARG_ADD_TEMPLATE = "add-template";
+ private ActivityTemplatesBinding b;
+ private NavController navController;
+ private ActivityResultLauncher<Void> qrScanLauncher;
+ private FabManager fabManager;
+ // @Override
+// public boolean onCreateOptionsMenu(Menu menu) {
+// super.onCreateOptionsMenu(menu);
+// getMenuInflater().inflate(R.menu.template_list_menu, menu);
+//
+// return true;
+// }
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ b = ActivityTemplatesBinding.inflate(getLayoutInflater());
+ setContentView(b.getRoot());
+ setSupportActionBar(b.toolbar);
+ // Show the Up button in the action bar.
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ NavHostFragment navHostFragment = (NavHostFragment) Objects.requireNonNull(
+ getSupportFragmentManager().findFragmentById(R.id.fragment_container));
+ navController = navHostFragment.getNavController();
+
+ navController.addOnDestinationChangedListener((controller, destination, arguments) -> {
+ if (destination.getId() == R.id.templateListFragment) {
+ b.toolbar.setTitle(getString(R.string.title_activity_templates));
+ b.fab.setImageResource(R.drawable.ic_add_white_24dp);
+ }
+ else {
+ b.fab.setImageResource(R.drawable.ic_save_white_24dp);
+ }
+ });
+
+ b.toolbar.setTitle(getString(R.string.title_activity_templates));
+
+ b.fab.setOnClickListener(v -> {
+ if (navController.getCurrentDestination()
+ .getId() == R.id.templateListFragment)
+ onEditTemplate(null);
+ else
+ onSaveTemplate();
+ });
+
+ qrScanLauncher = QR.registerLauncher(this, this);
+
+ fabManager = new FabManager(b.fab);
+ }
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ final NavDestination currentDestination = navController.getCurrentDestination();
+ if (currentDestination != null &&
+ currentDestination.getId() == R.id.templateDetailsFragment)
+ navController.popBackStack();
+ else
+ finish();
+
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ @Override
+ public void onDuplicateTemplate(long id) {
+ DB.get()
+ .getTemplateDAO()
+ .duplicateTemplateWithAccounts(id, null);
+ }
+ @Override
+ public void onEditTemplate(Long id) {
+ if (id == null) {
+ navController.navigate(R.id.action_templateListFragment_to_templateDetailsFragment);
+ b.toolbar.setTitle(getString(R.string.title_new_template));
+ }
+ else {
+ Bundle bundle = new Bundle();
+ bundle.putLong(TemplateDetailsFragment.ARG_TEMPLATE_ID, id);
+ navController.navigate(R.id.action_templateListFragment_to_templateDetailsFragment,
+ bundle);
+ b.toolbar.setTitle(getString(R.string.title_edit_template));
+ }
+ }
+ @Override
+ public void onSaveTemplate() {
+ final ViewModelStoreOwner viewModelStoreOwner =
+ navController.getViewModelStoreOwner(R.id.template_list_navigation);
+ TemplateDetailsViewModel model =
+ new ViewModelProvider(viewModelStoreOwner).get(TemplateDetailsViewModel.class);
+ Logger.debug("flow", "TemplatesActivity.onSavePattern(): model=" + model);
+ model.onSaveTemplate();
+ navController.navigateUp();
+ }
+ public NavController getNavController() {
+ return navController;
+ }
+ @Override
+ public void onDeleteTemplate(@NonNull Long templateId) {
+ Objects.requireNonNull(templateId);
+ TemplateHeaderDAO dao = DB.get()
+ .getTemplateDAO();
+
+ dao.getTemplateWithAccountsAsync(templateId, template -> {
+ TemplateWithAccounts copy = TemplateWithAccounts.from(template);
+ dao.deleteAsync(template.header, () -> {
+ navController.popBackStack(R.id.templateListFragment, false);
+
+ Snackbar.make(b.getRoot(), String.format(
+ TemplatesActivity.this.getString(R.string.template_xxx_deleted),
+ template.header.getName()), BaseTransientBottomBar.LENGTH_LONG)
+ .setAction(R.string.action_undo, v -> dao.insertAsync(copy, null))
+ .show();
+ });
+ });
+ }
+ @Override
+ public void onQRScanResult(String scanned) {
+ Logger.debug("PatDet_fr", String.format("Got scanned text '%s'", scanned));
+ TemplateDetailsViewModel model = new ViewModelProvider(
+ navController.getViewModelStoreOwner(R.id.template_list_navigation)).get(
+ TemplateDetailsViewModel.class);
+ model.setTestText(scanned);
+ }
+ @Override
+ public void triggerQRScan() {
+ qrScanLauncher.launch(null);
+ }
+ @Override
+ public Context getContext() {
+ return this;
+ }
+ @Override
+ public void showManagedFab() {
+ fabManager.showFab();
+ }
+ @Override
+ public void hideManagedFab() {
+ fabManager.hideFab();
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.templates;
+
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.RecyclerView;
+
+import net.ktnx.mobileledger.databinding.TemplateListTemplateItemBinding;
+import net.ktnx.mobileledger.databinding.TemplatesFallbackDividerBinding;
+import net.ktnx.mobileledger.db.TemplateHeader;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TemplatesRecyclerViewAdapter extends RecyclerView.Adapter<BaseTemplateViewHolder> {
+ static final int ITEM_TYPE_TEMPLATE = 1;
+ static final int ITEM_TYPE_DIVIDER = 2;
+ private final AsyncListDiffer<BaseTemplateItem> listDiffer;
+
+ public TemplatesRecyclerViewAdapter() {
+ listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<BaseTemplateItem>() {
+ @Override
+ public boolean areItemsTheSame(
+ @NotNull TemplatesRecyclerViewAdapter.BaseTemplateItem oldItem,
+ @NotNull TemplatesRecyclerViewAdapter.BaseTemplateItem newItem) {
+ return oldItem.getId() == newItem.getId();
+ }
+ @Override
+ public boolean areContentsTheSame(
+ @NotNull TemplatesRecyclerViewAdapter.BaseTemplateItem oldItem,
+ @NotNull TemplatesRecyclerViewAdapter.BaseTemplateItem newItem) {
+ return oldItem.equals(newItem);
+ }
+ });
+ }
+ @NonNull
+ @Override
+ public BaseTemplateViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ switch (viewType) {
+ case ITEM_TYPE_TEMPLATE:
+ TemplateListTemplateItemBinding b =
+ TemplateListTemplateItemBinding.inflate(inflater, parent, false);
+ return new BaseTemplateViewHolder.TemplateViewHolder(b);
+ case ITEM_TYPE_DIVIDER:
+ return new BaseTemplateViewHolder.TemplateDividerViewHolder(
+ TemplatesFallbackDividerBinding.inflate(inflater, parent, false));
+ default:
+ throw new RuntimeException("Can't handle " + viewType);
+ }
+ }
+ @Override
+ public void onBindViewHolder(@NonNull BaseTemplateViewHolder holder, int position) {
+ holder.bindToItem(listDiffer.getCurrentList()
+ .get(position));
+ }
+ @Override
+ public int getItemViewType(int position) {
+ BaseTemplateItem item = getItem(position);
+ if (item instanceof TemplateItem)
+ return ITEM_TYPE_TEMPLATE;
+ if (item instanceof TemplateDivider)
+ return ITEM_TYPE_DIVIDER;
+
+ throw new RuntimeException("Can't handle " + item);
+ }
+ @Override
+ public int getItemCount() {
+ return listDiffer.getCurrentList()
+ .size();
+ }
+ public void setTemplates(List<TemplateHeader> newList) {
+ List<BaseTemplateItem> itemList = new ArrayList<>();
+
+ boolean reachedFallbackItems = false;
+
+ for (TemplateHeader item : newList) {
+ if (!reachedFallbackItems && item.isFallback()) {
+ itemList.add(new TemplateDivider());
+ reachedFallbackItems = true;
+ }
+ itemList.add(new TemplateItem(item));
+ }
+
+ listDiffer.submitList(itemList);
+ }
+ public BaseTemplateItem getItem(int position) {
+ return listDiffer.getCurrentList()
+ .get(position);
+ }
+
+ static abstract class BaseTemplateItem {
+ abstract long getId();
+
+ abstract boolean equals(BaseTemplateItem other);
+ }
+
+ static class TemplateItem extends BaseTemplateItem {
+ final TemplateHeader template;
+ TemplateItem(TemplateHeader template) {this.template = template;}
+ @Override
+ long getId() {
+ return template.getId();
+ }
+ @Override
+ boolean equals(BaseTemplateItem other) {
+ return template.equals(((TemplateItem) other).template);
+ }
+ }
+
+ static class TemplateDivider extends BaseTemplateItem {
+ @Override
+ long getId() {
+ return -1;
+ }
+ @Override
+ boolean equals(BaseTemplateItem other) {
+ return true;
+ }
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.transaction_list;
-import android.app.Activity;
-import android.content.Context;
-import android.database.sqlite.SQLiteDatabase;
-import android.graphics.Typeface;
-import android.os.AsyncTask;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.style.StyleSpan;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
+import net.ktnx.mobileledger.databinding.LastUpdateLayoutBinding;
+import net.ktnx.mobileledger.databinding.TransactionDelimiterBinding;
+import net.ktnx.mobileledger.databinding.TransactionListRowBinding;
import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.ui.MainModel;
-import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.Logger;
import net.ktnx.mobileledger.utils.Misc;
-import net.ktnx.mobileledger.utils.SimpleDate;
-import java.text.DateFormat;
-import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
-import java.util.TimeZone;
-public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolder> {
- private final MainModel model;
+public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolderBase> {
private final AsyncListDiffer<TransactionListItem> listDiffer;
- public TransactionListAdapter(MainModel model) {
+ public TransactionListAdapter() {
super();
- this.model = model;
+
+ setHasStableIds(true);
listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TransactionListItem>() {
@Override
.equals(newItem.getDate()));
case TRANSACTION:
return oldItem.getTransaction()
- .getId() == newItem.getTransaction()
- .getId();
+ .getLedgerId() == newItem.getTransaction()
+ .getLedgerId();
case HEADER:
return true; // there can be only one header
default:
@NonNull TransactionListItem newItem) {
switch (oldItem.getType()) {
case DELIMITER:
- // Delimiters items are "same" for same dates and the contents are the date
- return true;
+ return oldItem.isMonthShown() == newItem.isMonthShown();
case TRANSACTION:
return oldItem.getTransaction()
- .equals(newItem.getTransaction());
+ .equals(newItem.getTransaction()) &&
+ Misc.equalStrings(oldItem.getBoldAccountName(),
+ newItem.getBoldAccountName()) &&
+ Misc.equalStrings(oldItem.getRunningTotal(),
+ newItem.getRunningTotal());
case HEADER:
// headers don't differ in their contents. they observe the last update
// date and react to its changes
}
});
}
- public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
+ @Override
+ public long getItemId(int position) {
+ TransactionListItem item = listDiffer.getCurrentList()
+ .get(position);
+ switch (item.getType()) {
+ case HEADER:
+ return -1;
+ case TRANSACTION:
+ return item.getTransaction()
+ .getLedgerId();
+ case DELIMITER:
+ return -item.getDate()
+ .toDate()
+ .getTime();
+ default:
+ throw new IllegalStateException("Unexpected value: " + item.getType());
+ }
+ }
+ @Override
+ public int getItemViewType(int position) {
+ return listDiffer.getCurrentList()
+ .get(position)
+ .getType()
+ .ordinal();
+ }
+ public void onBindViewHolder(@NonNull TransactionRowHolderBase holder, int position) {
TransactionListItem item = listDiffer.getCurrentList()
.get(position);
return;
final TransactionListItem.Type newType = item.getType();
- holder.setType(newType);
switch (newType) {
case TRANSACTION:
- LedgerTransaction tr = item.getTransaction();
-
- // debug("transactions", String.format("Filling position %d with %d
- // accounts", position,
- // tr.getAccounts().size()));
+ holder.asTransaction()
+ .bind(item, item.getBoldAccountName());
- TransactionLoader loader = new TransactionLoader();
- loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
- new TransactionLoaderParams(tr, holder, position, model.getAccountFilter()
- .getValue()));
-
- // WORKAROUND what seems to be a bug in CardHolder somewhere
- // when a view that was previously holding a delimiter is re-purposed
- // occasionally it stays too short (not high enough)
- holder.vTransaction.measure(
- View.MeasureSpec.makeMeasureSpec(holder.itemView.getWidth(),
- View.MeasureSpec.EXACTLY),
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
break;
case DELIMITER:
- SimpleDate date = item.getDate();
- holder.tvDelimiterDate.setText(DateFormat.getDateInstance()
- .format(date.toDate()));
- if (item.isMonthShown()) {
- GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
- cal.setTime(date.toDate());
- App.prepareMonthNames();
- holder.tvDelimiterMonth.setText(
- Globals.monthNames[cal.get(GregorianCalendar.MONTH)]);
- holder.tvDelimiterMonth.setVisibility(View.VISIBLE);
- // holder.vDelimiterLine.setBackgroundResource(R.drawable
- // .dashed_border_8dp);
- holder.vDelimiterThick.setVisibility(View.VISIBLE);
- }
- else {
- holder.tvDelimiterMonth.setVisibility(View.GONE);
- // holder.vDelimiterLine.setBackgroundResource(R.drawable
- // .dashed_border_1dp);
- holder.vDelimiterThick.setVisibility(View.GONE);
- }
+ holder.asDelimiter()
+ .bind(item);
break;
case HEADER:
- holder.setLastUpdateText(Data.lastTransactionsUpdateText.get());
+ holder.asHeader()
+ .bind();
break;
default:
}
@NonNull
@Override
- public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ public TransactionRowHolderBase onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// debug("perf", "onCreateViewHolder called");
- View row = LayoutInflater.from(parent.getContext())
- .inflate(R.layout.transaction_list_row, parent, false);
- return new TransactionRowHolder(row);
+ final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+ switch (TransactionListItem.Type.valueOf(viewType)) {
+ case TRANSACTION:
+ return new TransactionRowHolder(
+ TransactionListRowBinding.inflate(inflater, parent, false));
+ case DELIMITER:
+ return new TransactionListDelimiterRowHolder(
+ TransactionDelimiterBinding.inflate(inflater, parent, false));
+ case HEADER:
+ return new TransactionListLastUpdateRowHolder(
+ LastUpdateLayoutBinding.inflate(inflater, parent, false));
+ default:
+ throw new IllegalStateException("Unexpected value: " + viewType);
+ }
}
@Override
String.format(Locale.US, "Got new transaction list (%d items)", newList.size()));
listDiffer.submitList(newList);
}
- enum LoaderStep {HEAD, ACCOUNTS, DONE}
-
- private static class TransactionLoader
- extends AsyncTask<TransactionLoaderParams, TransactionLoaderStep, Void> {
- @Override
- protected Void doInBackground(TransactionLoaderParams... p) {
- LedgerTransaction tr = p[0].transaction;
-
- SQLiteDatabase db = App.getDatabase();
- tr.loadData(db);
-
- publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, tr));
-
- int rowIndex = 0;
- // FIXME ConcurrentModificationException in ArrayList$ltr.next (ArrayList.java:831)
- for (LedgerTransactionAccount acc : tr.getAccounts()) {
-// debug(c.getAccountName(), acc.getAmount()));
- publishProgress(new TransactionLoaderStep(p[0].holder, acc, rowIndex++,
- p[0].boldAccountName));
- }
-
- publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, rowIndex));
-
- return null;
- }
- @Override
- protected void onProgressUpdate(TransactionLoaderStep... values) {
- super.onProgressUpdate(values);
- TransactionLoaderStep step = values[0];
- TransactionRowHolder holder = step.getHolder();
-
- switch (step.getStep()) {
- case HEAD:
- holder.tvDescription.setText(step.getTransaction()
- .getDescription());
- String trComment = Misc.emptyIsNull(step.getTransaction()
- .getComment());
- if (trComment == null)
- holder.tvComment.setVisibility(View.GONE);
- else {
- holder.tvComment.setText(trComment);
- holder.tvComment.setVisibility(View.VISIBLE);
- }
-
-// if (step.isOdd())
-// holder.row.setBackgroundColor(Colors.tableRowDarkBG);
-// else
-// holder.row.setBackgroundColor(Colors.tableRowLightBG);
-
- break;
- case ACCOUNTS:
- int rowIndex = step.getAccountPosition();
- Context ctx = holder.row.getContext();
- LinearLayout row = (LinearLayout) holder.tableAccounts.getChildAt(rowIndex);
- if (row == null) {
- row = new LinearLayout(ctx);
- LayoutInflater inflater = ((Activity) ctx).getLayoutInflater();
- inflater.inflate(R.layout.transaction_list_row_accounts_table_row, row);
- holder.tableAccounts.addView(row);
- }
- TextView dummyText = row.findViewById(R.id.dummy_text);
- TextView accName = row.findViewById(R.id.transaction_list_acc_row_acc_name);
- TextView accComment =
- row.findViewById(R.id.transaction_list_acc_row_acc_comment);
- TextView accAmount = row.findViewById(R.id.transaction_list_acc_row_acc_amount);
- LedgerTransactionAccount acc = step.getAccount();
-
-
-// debug("tmp", String.format("showing acc row %d: %s %1.2f", rowIndex,
-// acc.getAccountName(), acc.getAmount()));
-
- String boldAccountName = step.getBoldAccountName();
- if ((boldAccountName != null) && acc.getAccountName()
- .startsWith(boldAccountName))
- {
- accName.setTextColor(Colors.secondary);
- accAmount.setTextColor(Colors.secondary);
-
- SpannableString ss = new SpannableString(acc.getAccountName());
- ss.setSpan(new StyleSpan(Typeface.BOLD), 0, boldAccountName.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- accName.setText(ss);
- }
- else {
- @ColorInt int textColor = dummyText.getTextColors()
- .getDefaultColor();
- accName.setTextColor(textColor);
- accAmount.setTextColor(textColor);
- accName.setText(acc.getAccountName());
- }
-
- String comment = acc.getComment();
- if (comment != null && !comment.isEmpty()) {
- accComment.setText(comment);
- accComment.setVisibility(View.VISIBLE);
- }
- else {
- accComment.setVisibility(View.GONE);
- }
- accAmount.setText(acc.toString());
-
- break;
- case DONE:
- int accCount = step.getAccountCount();
- if (holder.tableAccounts.getChildCount() > accCount) {
- holder.tableAccounts.removeViews(accCount,
- holder.tableAccounts.getChildCount() - accCount);
- }
-
-// debug("transactions",
-// String.format("Position %d fill done", step.getPosition()));
- }
- }
- }
-
- private static class TransactionLoaderParams {
- final LedgerTransaction transaction;
- final TransactionRowHolder holder;
- final int position;
- final String boldAccountName;
- TransactionLoaderParams(LedgerTransaction transaction, TransactionRowHolder holder,
- int position, String boldAccountName) {
- this.transaction = transaction;
- this.holder = holder;
- this.position = position;
- this.boldAccountName = boldAccountName;
- }
- }
}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright © 2024 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import android.view.View;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import net.ktnx.mobileledger.App;
+import net.ktnx.mobileledger.databinding.TransactionDelimiterBinding;
+import net.ktnx.mobileledger.model.TransactionListItem;
+import net.ktnx.mobileledger.utils.DimensionUtils;
+import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.text.DateFormat;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+class TransactionListDelimiterRowHolder extends TransactionRowHolderBase {
+ private final TransactionDelimiterBinding b;
+ TransactionListDelimiterRowHolder(TransactionDelimiterBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ public void bind(TransactionListItem item) {
+ SimpleDate date = item.getDate();
+ b.transactionDelimiterDate.setText(DateFormat.getDateInstance()
+ .format(date.toDate()));
+ if (item.isMonthShown()) {
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
+ cal.setTime(date.toDate());
+ App.prepareMonthNames();
+ b.transactionDelimiterMonth.setText(
+ Globals.monthNames[cal.get(GregorianCalendar.MONTH)]);
+ b.transactionDelimiterMonth.setVisibility(View.VISIBLE);
+ b.transactionDelimiterThick.setVisibility(View.VISIBLE);
+ ConstraintLayout.LayoutParams lp =
+ (ConstraintLayout.LayoutParams) b.transactionDelimiterThick.getLayoutParams();
+ lp.height = DimensionUtils.dp2px(b.getRoot()
+ .getContext(), 4);
+ b.transactionDelimiterThick.setLayoutParams(lp);
+ }
+ else {
+ b.transactionDelimiterMonth.setVisibility(View.GONE);
+ ConstraintLayout.LayoutParams lp =
+ (ConstraintLayout.LayoutParams) b.transactionDelimiterThick.getLayoutParams();
+ lp.height = DimensionUtils.dp2px(b.getRoot()
+ .getContext(), 1.3f);
+ b.transactionDelimiterThick.setLayoutParams(lp);
+ b.transactionDelimiterThick.setVisibility(View.VISIBLE);
+ }
+
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.transaction_list;
-import android.database.Cursor;
-import android.os.AsyncTask;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
-import android.widget.AutoCompleteTextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import net.ktnx.mobileledger.R;
import net.ktnx.mobileledger.async.TransactionDateFinder;
+import net.ktnx.mobileledger.databinding.TransactionListFragmentBinding;
+import net.ktnx.mobileledger.db.AccountAutocompleteAdapter;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
import net.ktnx.mobileledger.ui.DatePickerFragment;
+import net.ktnx.mobileledger.ui.FabManager;
import net.ktnx.mobileledger.ui.MainModel;
import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
import net.ktnx.mobileledger.ui.activity.MainActivity;
import net.ktnx.mobileledger.utils.Colors;
import net.ktnx.mobileledger.utils.Globals;
import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
import net.ktnx.mobileledger.utils.SimpleDate;
import org.jetbrains.annotations.NotNull;
public class TransactionListFragment extends MobileLedgerListFragment
implements DatePickerFragment.DatePickedListener {
private MenuItem menuTransactionListFilter;
- private View vAccountFilter;
- private AutoCompleteTextView accNameFilter;
+ private MenuItem menuGoToDate;
private MainModel model;
+ private boolean fragmentActive = false;
+ private TransactionListFragmentBinding b;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.transaction_list_fragment, container, false);
+ b = TransactionListFragmentBinding.inflate(inflater, container, false);
+ return b.getRoot();
}
@Override
public void onResume() {
super.onResume();
+ fragmentActive = true;
+ toggleMenuItems();
debug("flow", "TransactionListFragment.onResume()");
}
+ private void toggleMenuItems() {
+ if (menuGoToDate != null)
+ menuGoToDate.setVisible(fragmentActive);
+ if (menuTransactionListFilter != null) {
+ final int filterVisibility = b.transactionListAccountNameFilter.getVisibility();
+ menuTransactionListFilter.setVisible(
+ fragmentActive && filterVisibility != View.VISIBLE);
+ }
+ }
@Override
public void onStop() {
super.onStop();
+ fragmentActive = false;
+ toggleMenuItems();
debug("flow", "TransactionListFragment.onStop()");
}
@Override
public void onPause() {
super.onPause();
+ fragmentActive = false;
+ toggleMenuItems();
debug("flow", "TransactionListFragment.onPause()");
}
@Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ public SwipeRefreshLayout getRefreshLayout() {
+ return b.transactionSwipe;
+ }
+ @Override
+ public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
debug("flow", "TransactionListFragment.onActivityCreated called");
- super.onActivityCreated(savedInstanceState);
-
+ super.onViewCreated(view, savedInstanceState);
Data.backgroundTasksRunning.observe(getViewLifecycleOwner(),
this::onBackgroundTaskRunningChanged);
model = new ViewModelProvider(requireActivity()).get(MainModel.class);
- refreshLayout = mainActivity.findViewById(R.id.transaction_swipe);
- if (refreshLayout == null)
- throw new RuntimeException("Can't get hold on the swipe layout");
- root = mainActivity.findViewById(R.id.transaction_root);
- if (root == null)
- throw new RuntimeException("Can't get hold on the transaction value view");
- modelAdapter = new TransactionListAdapter(model);
- root.setAdapter(modelAdapter);
+ modelAdapter = new TransactionListAdapter();
+ b.transactionRoot.setAdapter(modelAdapter);
mainActivity.fabShouldShow();
- manageFabOnScroll();
+ if (mainActivity instanceof FabManager.FabHandler)
+ FabManager.handle(mainActivity, b.transactionRoot);
LinearLayoutManager llm = new LinearLayoutManager(mainActivity);
llm.setOrientation(RecyclerView.VERTICAL);
- root.setLayoutManager(llm);
+ b.transactionRoot.setLayoutManager(llm);
- refreshLayout.setOnRefreshListener(() -> {
+ b.transactionSwipe.setOnRefreshListener(() -> {
debug("ui", "refreshing transactions via swipe");
model.scheduleTransactionListRetrieval();
});
Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
- vAccountFilter = mainActivity.findViewById(R.id.transaction_list_account_name_filter);
- accNameFilter = mainActivity.findViewById(R.id.transaction_filter_account_name);
+ Data.observeProfile(getViewLifecycleOwner(), this::onProfileChanged);
- MLDB.hookAutocompletionAdapter(mainActivity, accNameFilter, "accounts", "name");
- accNameFilter.setOnItemClickListener((parent, view, position, id) -> {
+ b.transactionFilterAccountName.setOnItemClickListener((parent, v, position, id) -> {
// debug("tmp", "direct onItemClick");
- Cursor c = (Cursor) parent.getItemAtPosition(position);
model.getAccountFilter()
- .setValue(c.getString(1));
+ .setValue(parent.getItemAtPosition(position)
+ .toString());
Globals.hideSoftKeyboard(mainActivity);
});
.observe(getViewLifecycleOwner(), this::onAccountNameFilterChanged);
model.getUpdatingFlag()
- .observe(getViewLifecycleOwner(), (flag) -> refreshLayout.setRefreshing(flag));
- MobileLedgerProfile profile = Data.getProfile();
+ .observe(getViewLifecycleOwner(), (flag) -> b.transactionSwipe.setRefreshing(flag));
model.getDisplayedTransactions()
.observe(getViewLifecycleOwner(), list -> modelAdapter.setTransactions(list));
- mainActivity.findViewById(R.id.clearAccountNameFilter)
- .setOnClickListener(v -> {
- model.getAccountFilter()
- .setValue(null);
- vAccountFilter.setVisibility(View.GONE);
- menuTransactionListFilter.setVisible(true);
- Globals.hideSoftKeyboard(mainActivity);
- });
+ view.findViewById(R.id.clearAccountNameFilter)
+ .setOnClickListener(v -> {
+ model.getAccountFilter()
+ .setValue(null);
+ Globals.hideSoftKeyboard(mainActivity);
+ });
model.foundTransactionItemIndex.observe(getViewLifecycleOwner(), pos -> {
Logger.debug("go-to-date", String.format(Locale.US, "Found pos %d", pos));
if (pos != null) {
- root.scrollToPosition(pos);
+ b.transactionRoot.scrollToPosition(pos);
// reset the value to avoid re-notification upon reconfiguration or app restart
model.foundTransactionItemIndex.setValue(null);
}
});
}
- private void onAccountNameFilterChanged(String accName) {
- final String fieldText = accNameFilter.getText()
- .toString();
- if ((accName == null) && (fieldText.equals("")))
+ private void onProfileChanged(Profile profile) {
+ if (profile == null)
return;
- if (accNameFilter != null) {
- accNameFilter.setText(accName, false);
- }
- final boolean filterActive = (accName != null) && !accName.isEmpty();
- if (vAccountFilter != null) {
- vAccountFilter.setVisibility(filterActive ? View.VISIBLE : View.GONE);
- }
+ b.transactionFilterAccountName.setAdapter(
+ new AccountAutocompleteAdapter(getContext(), profile));
+ }
+ private void onAccountNameFilterChanged(String accName) {
+ b.transactionFilterAccountName.setText(accName, false);
+
+ boolean filterActive = (accName != null) && !accName.isEmpty();
+ b.transactionListAccountNameFilter.setVisibility(filterActive ? View.VISIBLE : View.GONE);
if (menuTransactionListFilter != null)
menuTransactionListFilter.setVisible(!filterActive);
-
- model.scheduleTransactionListReload();
-
}
@Override
public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
menuTransactionListFilter = menu.findItem(R.id.menu_transaction_list_filter);
if ((menuTransactionListFilter == null))
throw new AssertionError();
+ menuGoToDate = menu.findItem(R.id.menu_go_to_date);
+ if ((menuGoToDate == null))
+ throw new AssertionError();
- if ((model.getAccountFilter()
- .getValue() != null) || (vAccountFilter.getVisibility() == View.VISIBLE))
- {
- menuTransactionListFilter.setVisible(false);
- }
+ model.getAccountFilter()
+ .observe(this, v -> menuTransactionListFilter.setVisible(v == null));
super.onCreateOptionsMenu(menu, inflater);
menuTransactionListFilter.setOnMenuItemClickListener(item -> {
- vAccountFilter.setVisibility(View.VISIBLE);
- if (menuTransactionListFilter != null)
- menuTransactionListFilter.setVisible(false);
- accNameFilter.requestFocus();
+ b.transactionListAccountNameFilter.setVisibility(View.VISIBLE);
+ menuTransactionListFilter.setVisible(false);
+ b.transactionFilterAccountName.requestFocus();
InputMethodManager imm =
(InputMethodManager) getMainActivity().getSystemService(INPUT_METHOD_SERVICE);
- imm.showSoftInput(accNameFilter, 0);
+ imm.showSoftInput(b.transactionFilterAccountName, 0);
return true;
});
- menu.findItem(R.id.menu_go_to_date)
- .setOnMenuItemClickListener(item -> {
- DatePickerFragment picker = new DatePickerFragment();
- picker.setOnDatePickedListener(this);
- picker.setDateRange(model.getFirstTransactionDate(),
- model.getLastTransactionDate());
- picker.show(requireActivity().getSupportFragmentManager(), null);
- return true;
- });
+ menuGoToDate.setOnMenuItemClickListener(item -> {
+ DatePickerFragment picker = new DatePickerFragment();
+ picker.setOnDatePickedListener(this);
+ picker.setDateRange(model.getFirstTransactionDate(), model.getLastTransactionDate());
+ picker.show(requireActivity().getSupportFragmentManager(), null);
+ return true;
+ });
+
+ toggleMenuItems();
}
@Override
public void onDatePicked(int year, int month, int day) {
RecyclerView list = requireActivity().findViewById(R.id.transaction_root);
- AsyncTask<TransactionDateFinder.Params, Void, Integer> finder = new TransactionDateFinder();
+ TransactionDateFinder finder = new TransactionDateFinder(model, new SimpleDate(year, month + 1, day));
- finder.execute(
- new TransactionDateFinder.Params(model, new SimpleDate(year, month + 1, day)));
+ finder.start();
}
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import net.ktnx.mobileledger.databinding.LastUpdateLayoutBinding;
+import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
+
+class TransactionListLastUpdateRowHolder extends TransactionRowHolderBase {
+ private final LastUpdateLayoutBinding b;
+ TransactionListLastUpdateRowHolder(LastUpdateLayoutBinding binding) {
+ super(binding.getRoot());
+ b = binding;
+ }
+ void setLastUpdateText(String text) {
+ b.lastUpdateText.setText(text);
+ }
+ public void bind() {
+ Data.lastTransactionsUpdateText.observe((MainActivity) b.lastUpdateText.getContext(),
+ b.lastUpdateText::setText);
+ }
+}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.ui.transaction_list;
-
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-
-class TransactionLoaderStep {
- private final TransactionListAdapter.LoaderStep step;
- private final TransactionRowHolder holder;
- private int position;
- private int accountCount;
- private LedgerTransaction transaction;
- private LedgerTransactionAccount account;
- private int accountPosition;
- private String boldAccountName;
- public TransactionLoaderStep(TransactionRowHolder holder, int position,
- LedgerTransaction transaction) {
- this.step = TransactionListAdapter.LoaderStep.HEAD;
- this.holder = holder;
- this.transaction = transaction;
- this.position = position;
- }
- public TransactionLoaderStep(TransactionRowHolder holder, LedgerTransactionAccount account,
- int accountPosition, String boldAccountName) {
- this.step = TransactionListAdapter.LoaderStep.ACCOUNTS;
- this.holder = holder;
- this.account = account;
- this.accountPosition = accountPosition;
- this.boldAccountName = boldAccountName;
- }
- public TransactionLoaderStep(TransactionRowHolder holder, int position, int accountCount) {
- this.step = TransactionListAdapter.LoaderStep.DONE;
- this.holder = holder;
- this.position = position;
- this.accountCount = accountCount;
- }
- public int getAccountCount() {
- return accountCount;
- }
- public int getPosition() {
- return position;
- }
- public String getBoldAccountName() {
- return boldAccountName;
- }
- public int getAccountPosition() {
- return accountPosition;
- }
- public TransactionRowHolder getHolder() {
- return holder;
- }
- public TransactionListAdapter.LoaderStep getStep() {
- return step;
- }
- public LedgerTransaction getTransaction() {
- return transaction;
- }
- public LedgerTransactionAccount getAccount() {
- return account;
- }
-}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.ui.transaction_list;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
-import androidx.cardview.widget.CardView;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.annotation.Nullable;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
+import net.ktnx.mobileledger.databinding.TransactionListRowBinding;
+import net.ktnx.mobileledger.model.LedgerTransaction;
+import net.ktnx.mobileledger.model.LedgerTransactionAccount;
import net.ktnx.mobileledger.model.TransactionListItem;
+import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.Misc;
import java.util.Observer;
-class TransactionRowHolder extends RecyclerView.ViewHolder {
- final TextView tvDescription;
- final TextView tvComment;
- final LinearLayout tableAccounts;
- final ConstraintLayout row;
- final ConstraintLayout vDelimiter;
- final CardView vTransaction;
- final TextView tvDelimiterMonth, tvDelimiterDate;
- final View vDelimiterThick;
- final View vHeader;
- final TextView tvLastUpdate;
+class TransactionRowHolder extends TransactionRowHolderBase {
+ private final TransactionListRowBinding b;
TransactionListItem.Type lastType;
private Observer lastUpdateObserver;
- public TransactionRowHolder(@NonNull View itemView) {
- super(itemView);
- this.row = itemView.findViewById(R.id.transaction_row);
- this.tvDescription = itemView.findViewById(R.id.transaction_row_description);
- this.tvComment = itemView.findViewById(R.id.transaction_comment);
- this.tableAccounts = itemView.findViewById(R.id.transaction_row_acc_amounts);
- this.vDelimiter = itemView.findViewById(R.id.transaction_delimiter);
- this.vTransaction = itemView.findViewById(R.id.transaction_card_view);
- this.tvDelimiterDate = itemView.findViewById(R.id.transaction_delimiter_date);
- this.tvDelimiterMonth = itemView.findViewById(R.id.transaction_delimiter_month);
- this.vDelimiterThick = itemView.findViewById(R.id.transaction_delimiter_thick);
- this.vHeader = itemView.findViewById(R.id.last_update_container);
- this.tvLastUpdate = itemView.findViewById(R.id.last_update_text);
+ public TransactionRowHolder(@NonNull TransactionListRowBinding binding) {
+ super(binding.getRoot());
+ b = binding;
}
- private void initLastUpdateObserver() {
- if (lastUpdateObserver != null)
- return;
+ public void bind(@NonNull TransactionListItem item, @Nullable String boldAccountName) {
+ LedgerTransaction tr = item.getTransaction();
+ b.transactionRowDescription.setText(tr.getDescription());
+ String trComment = Misc.emptyIsNull(tr.getComment());
+ if (trComment == null)
+ b.transactionComment.setVisibility(View.GONE);
+ else {
+ b.transactionComment.setText(trComment);
+ b.transactionComment.setVisibility(View.VISIBLE);
+ }
- lastUpdateObserver = (o, arg) -> setLastUpdateText(Data.lastTransactionsUpdateText.get());
+ if (Misc.emptyIsNull(item.getRunningTotal()) != null) {
+ b.transactionRunningTotal.setText(item.getRunningTotal());
+ b.transactionRunningTotal.setVisibility(View.VISIBLE);
+ b.transactionRunningTotalDivider.setVisibility(View.VISIBLE);
+ }
+ else {
+ b.transactionRunningTotal.setVisibility(View.GONE);
+ b.transactionRunningTotalDivider.setVisibility(View.GONE);
+ }
- Data.lastTransactionsUpdateText.addObserver(lastUpdateObserver);
- }
- void setLastUpdateText(String text) {
- tvLastUpdate.setText(text);
- }
- private void dropLastUpdateObserver() {
- if (lastUpdateObserver == null)
- return;
+ int rowIndex = 0;
+ Context ctx = b.getRoot()
+ .getContext();
+ LayoutInflater inflater = ((Activity) ctx).getLayoutInflater();
+ for (LedgerTransactionAccount acc : tr.getAccounts()) {
+ LinearLayout row = (LinearLayout) b.transactionRowAccAmounts.getChildAt(rowIndex);
+ if (row == null) {
+ row = new LinearLayout(ctx);
+ inflater.inflate(R.layout.transaction_list_row_accounts_table_row, row);
+ b.transactionRowAccAmounts.addView(row);
+ }
- Data.lastTransactionsUpdateText.deleteObserver(lastUpdateObserver);
- lastUpdateObserver = null;
- }
- void setType(TransactionListItem.Type newType) {
- if (newType == lastType)
- return;
+ TextView dummyText = row.findViewById(R.id.dummy_text);
+ TextView accName = row.findViewById(R.id.transaction_list_acc_row_acc_name);
+ TextView accComment = row.findViewById(R.id.transaction_list_acc_row_acc_comment);
+ TextView accAmount = row.findViewById(R.id.transaction_list_acc_row_acc_amount);
+
+ if ((boldAccountName != null) && acc.getAccountName()
+ .startsWith(boldAccountName))
+ {
+ accName.setTextColor(Colors.primary);
+ accAmount.setTextColor(Colors.primary);
+
+ SpannableString ss = new SpannableString(Misc.addWrapHints(acc.getAccountName()));
+ ss.setSpan(new StyleSpan(Typeface.BOLD), 0, Misc.addWrapHints(boldAccountName)
+ .length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ accName.setText(ss);
+ }
+ else {
+ @ColorInt int textColor = dummyText.getTextColors()
+ .getDefaultColor();
+ accName.setTextColor(textColor);
+ accAmount.setTextColor(textColor);
+ accName.setText(Misc.addWrapHints(acc.getAccountName()));
+ }
- switch (newType) {
- case TRANSACTION:
- vHeader.setVisibility(View.GONE);
- vTransaction.setVisibility(View.VISIBLE);
- vDelimiter.setVisibility(View.GONE);
- dropLastUpdateObserver();
- break;
- case DELIMITER:
- vHeader.setVisibility(View.GONE);
- vTransaction.setVisibility(View.GONE);
- vDelimiter.setVisibility(View.VISIBLE);
- dropLastUpdateObserver();
- break;
- case HEADER:
- vHeader.setVisibility(View.VISIBLE);
- vTransaction.setVisibility(View.GONE);
- vDelimiter.setVisibility(View.GONE);
- initLastUpdateObserver();
- break;
- default:
- throw new IllegalStateException("Unexpected value: " + newType);
+ String comment = acc.getComment();
+ if (comment != null && !comment.isEmpty()) {
+ accComment.setText(comment);
+ accComment.setVisibility(View.VISIBLE);
+ }
+ else {
+ accComment.setVisibility(View.GONE);
+ }
+ accAmount.setText(acc.toString());
+
+ rowIndex++;
}
- lastType = newType;
+ if (b.transactionRowAccAmounts.getChildCount() > rowIndex) {
+ b.transactionRowAccAmounts.removeViews(rowIndex,
+ b.transactionRowAccAmounts.getChildCount() - rowIndex);
+ }
}
}
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.ui.transaction_list;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+public class TransactionRowHolderBase extends RecyclerView.ViewHolder {
+ public TransactionRowHolderBase(@NonNull View itemView) {
+ super(itemView);
+ }
+ public TransactionListLastUpdateRowHolder asHeader() {
+ return (TransactionListLastUpdateRowHolder) this;
+ }
+ public TransactionRowHolder asTransaction() {
+ return (TransactionRowHolder) this;
+ }
+ public TransactionListDelimiterRowHolder asDelimiter() {
+ return (TransactionListDelimiterRowHolder) this;
+ }
+}
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2024 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
package net.ktnx.mobileledger.utils;
+import static net.ktnx.mobileledger.utils.Logger.debug;
+
import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import net.ktnx.mobileledger.BuildConfig;
import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.db.Profile;
import net.ktnx.mobileledger.ui.HueRing;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Objects;
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
public class Colors {
public static final int DEFAULT_HUE_DEG = 261;
public static final MutableLiveData<Integer> themeWatch = new MutableLiveData<>(0);
};
private static final HashMap<Integer, Integer> themePrimaryColor = new HashMap<>();
public static @ColorInt
- int secondary;
+ int primary;
@ColorInt
public static int tableRowDarkBG;
- public static int profileThemeId = -1;
+ public static int profileThemeId = DEFAULT_HUE_DEG;
public static void refreshColors(Resources.Theme theme) {
TypedValue tv = new TypedValue();
theme.resolveAttribute(R.attr.table_row_dark_bg, tv, true);
tableRowDarkBG = tv.data;
- theme.resolveAttribute(R.attr.colorSecondary, tv, true);
- secondary = tv.data;
+ theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, tv, true);
+ primary = tv.data;
if (themePrimaryColor.size() == 0) {
for (int themeId : themeIDs) {
Resources.Theme tmpTheme = theme.getResources()
.newTheme();
tmpTheme.applyStyle(themeId, true);
- tmpTheme.resolveAttribute(R.attr.colorPrimary, tv, false);
+ tmpTheme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, tv, false);
themePrimaryColor.put(themeId, tv.data);
}
}
}
return colors;
}
- public static int getNewProfileThemeHue(ArrayList<MobileLedgerProfile> profiles) {
+ public static int getNewProfileThemeHue(List<Profile> profiles) {
if ((profiles == null) || (profiles.size() == 0))
return DEFAULT_HUE_DEG;
if (profiles.size() == 1) {
int opposite = profiles.get(0)
- .getThemeHue() + 180;
+ .getTheme() + 180;
opposite %= 360;
chosenHue = opposite;
}
else {
ArrayList<Integer> hues = new ArrayList<>();
- for (MobileLedgerProfile p : profiles) {
- int hue = p.getThemeHue();
+ for (Profile p : profiles) {
+ int hue = p.getTheme();
if (hue == -1)
hue = DEFAULT_HUE_DEG;
hues.add(hue);
huesSB.append(", ");
huesSB.append(h);
}
- debug("profiles", String.format("used hues: %s", huesSB.toString()));
+ debug("profiles", String.format("used hues: %s", huesSB));
}
hues.add(hues.get(0));
+++ /dev/null
-/*
- * Copyright © 2019 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.utils;
-
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.widget.AutoCompleteTextView;
-import android.widget.FilterQueryProvider;
-import android.widget.SimpleCursorAdapter;
-
-import androidx.annotation.NonNull;
-
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.async.DbOpQueue;
-import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-
-import org.jetbrains.annotations.NonNls;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public final class MLDB {
- public static final String ACCOUNTS_TABLE = "accounts";
- public static final String DESCRIPTION_HISTORY_TABLE = "description_history";
- public static final String OPT_LAST_SCRAPE = "last_scrape";
- @NonNls
- public static final String OPT_PROFILE_UUID = "profile_uuid";
- private static final String NO_PROFILE = "-";
- @SuppressWarnings("unused")
- static public int getIntOption(String name, int default_value) {
- String s = getOption(name, String.valueOf(default_value));
- try {
- return Integer.parseInt(s);
- }
- catch (Exception e) {
- debug("db", "returning default int value of " + name, e);
- return default_value;
- }
- }
- @SuppressWarnings("unused")
- static public long getLongOption(String name, long default_value) {
- String s = getOption(name, String.valueOf(default_value));
- try {
- return Long.parseLong(s);
- }
- catch (Exception e) {
- debug("db", "returning default long value of " + name, e);
- return default_value;
- }
- }
- static public void getOption(String name, String defaultValue, GetOptCallback cb) {
- AsyncTask<Void, Void, String> t = new AsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackground(Void... params) {
- SQLiteDatabase db = App.getDatabase();
- try (Cursor cursor = db.rawQuery(
- "select value from options where profile=? and name=?",
- new String[]{NO_PROFILE, name}))
- {
- if (cursor.moveToFirst()) {
- String result = cursor.getString(0);
-
- if (result == null)
- result = defaultValue;
-
- debug("async-db", "option " + name + "=" + result);
- return result;
- }
- else
- return defaultValue;
- }
- catch (Exception e) {
- debug("db", "returning default value for " + name, e);
- return defaultValue;
- }
- }
- @Override
- protected void onPostExecute(String result) {
- cb.onResult(result);
- }
- };
-
- t.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void) null);
- }
- static public String getOption(String name, String default_value) {
- debug("db", "about to fetch option " + name);
- SQLiteDatabase db = App.getDatabase();
- try (Cursor cursor = db.rawQuery("select value from options where profile=? and name=?",
- new String[]{NO_PROFILE, name}))
- {
- if (cursor.moveToFirst()) {
- String result = cursor.getString(0);
-
- if (result == null)
- result = default_value;
-
- debug("db", "option " + name + "=" + result);
- return result;
- }
- else
- return default_value;
- }
- catch (Exception e) {
- debug("db", "returning default value for " + name, e);
- return default_value;
- }
- }
- static public void setOption(String name, String value) {
- debug("option", String.format("%s := %s", name, value));
- DbOpQueue.add("insert or replace into options(profile, name, value) values(?, ?, ?);",
- new String[]{NO_PROFILE, name, value});
- }
- @SuppressWarnings("unused")
- static public void setLongOption(String name, long value) {
- setOption(name, String.valueOf(value));
- }
- @TargetApi(Build.VERSION_CODES.N)
- public static void hookAutocompletionAdapter(final Context context,
- final AutoCompleteTextView view,
- final String table, final String field) {
- hookAutocompletionAdapter(context, view, table, field, true, null, null);
- }
- @TargetApi(Build.VERSION_CODES.N)
- public static void hookAutocompletionAdapter(final Context context,
- final AutoCompleteTextView view,
- final String table, final String field,
- final boolean profileSpecific,
- final DescriptionSelectedCallback callback,
- final MobileLedgerProfile profile) {
- String[] from = {field};
- int[] to = {android.R.id.text1};
- SimpleCursorAdapter adapter =
- new SimpleCursorAdapter(context, android.R.layout.simple_dropdown_item_1line, null,
- from, to, 0);
- adapter.setStringConversionColumn(1);
-
- FilterQueryProvider provider = constraint -> {
- if (constraint == null)
- return null;
-
- String str = constraint.toString()
- .toUpperCase();
- debug("autocompletion", "Looking for " + str);
-
- String sql;
- String[] params;
- if (profileSpecific) {
- MobileLedgerProfile p = (profile == null) ? Data.getProfile() : profile;
- sql = String.format(
- "SELECT rowid as _id, %s, CASE WHEN %s_upper LIKE ?||'%%' THEN 1 " +
- "WHEN %s_upper LIKE '%%:'||?||'%%' then 2 " +
- "WHEN %s_upper LIKE '%% '||?||'%%' THEN 3 " + "ELSE 9 END " + "FROM %s " +
- "WHERE profile=? AND %s_upper LIKE '%%'||?||'%%' " +
- "ORDER BY 3, %s_upper, 1;", field, field, field, field, table, field,
- field);
- params = new String[]{str, str, str, p.getUuid(), str};
- }
- else {
- sql = String.format(
- "SELECT rowid as _id, %s, CASE WHEN %s_upper LIKE ?||'%%' THEN 1 " +
- "WHEN %s_upper LIKE '%%:'||?||'%%' THEN 2 " +
- "WHEN %s_upper LIKE '%% '||?||'%%' THEN 3 " + "ELSE 9 END " + "FROM %s " +
- "WHERE %s_upper LIKE '%%'||?||'%%' " + "ORDER BY 3, %s_upper, 1;", field,
- field, field, field, table, field, field);
- params = new String[]{str, str, str, str};
- }
- debug("autocompletion", sql);
- SQLiteDatabase db = App.getDatabase();
-
- return db.rawQuery(sql, params);
- };
-
- adapter.setFilterQueryProvider(provider);
-
- view.setAdapter(adapter);
-
- if (callback != null)
- view.setOnItemClickListener(
- (parent, itemView, position, id) -> callback.descriptionSelected(
- String.valueOf(view.getText())));
- }
- public static void queryInBackground(@NonNull String statement, @NonNull String[] params,
- @NonNull CallbackHelper callbackHelper) {
- /* All callbacks are called in the new (asynchronous) thread! */
- Thread t = new Thread(() -> {
- callbackHelper.onStart();
- try {
- SQLiteDatabase db = App.getDatabase();
-
- try (Cursor cursor = db.rawQuery(statement, params)) {
- boolean gotRow = false;
- while (cursor.moveToNext()) {
- gotRow = true;
- if (!callbackHelper.onRow(cursor))
- break;
- }
- if (!gotRow) {
- callbackHelper.onNoRows();
- }
- }
- }
- catch (Exception e) {
- callbackHelper.onException(e);
- }
- finally {
- callbackHelper.onDone();
- }
- });
-
- t.start();
- }
- /* MLDB.CallbackHelper -- Abstract class for asynchronous SQL query callbacks */
- @SuppressWarnings("WeakerAccess")
- abstract public static class CallbackHelper {
- public void onStart() {}
- public abstract boolean onRow(@NonNull Cursor cursor);
- public void onNoRows() {}
- public void onException(Exception exception) {
- Logger.debug("MLDB", "Exception in asynchronous SQL", exception);
- }
- public void onDone() {}
- }
-}
-
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2021 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
import android.app.Activity;
import android.content.res.Configuration;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Editable;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import org.jetbrains.annotations.Contract;
+
public class Misc {
+ public static final char ZERO_WIDTH_SPACE = '\u200B';
public static boolean isZero(float f) {
return (f < 0.005) && (f > -0.005);
}
+ public static boolean equalFloats(float a, float b) { return isZero(a - b); }
public static void showSoftKeyboard(Activity activity) {
// make the keyboard appear
Configuration cf = activity.getResources()
}
public static String emptyIsNull(String str) {
- return "".equals(str) ? null : str;
+ return str != null && str.isEmpty() ? null : str;
}
public static String nullIsEmpty(String str) {
return (str == null) ? "" : str;
}
+ public static String nullIsEmpty(Editable e) {
+ if (e == null)
+ return "";
+ return e.toString();
+ }
public static boolean equalStrings(String u, CharSequence text) {
return nullIsEmpty(u).equals(text.toString());
}
return string.trim();
}
+ @Contract(value = "null, null -> true; null, !null -> false; !null, null -> false", pure = true)
+ public static boolean equalIntegers(Integer a, Integer b) {
+ if (a == null && b == null)
+ return true;
+ if (a == null || b == null)
+ return false;
+
+ return a.equals(b);
+ }
+ @Contract(value = "null, null -> true; null, !null -> false; !null, null -> false", pure = true)
+ public static boolean equalLongs(Long a, Long b) {
+ if (a == null && b == null)
+ return true;
+ if (a == null || b == null)
+ return false;
+
+ return a.equals(b);
+ }
+ public static void onMainThread(Runnable r) {
+ new Handler(Looper.getMainLooper()).post(r);
+ }
+ public static String addWrapHints(String input) {
+ if (input == null)
+ return null;
+ StringBuilder result = new StringBuilder();
+ int lastPos = 0;
+ int pos = input.indexOf(':');
+
+ while (pos >= 0) {
+ result.append(input.substring(lastPos, pos + 1))
+ .append(ZERO_WIDTH_SPACE);
+ lastPos = pos + 1;
+ pos = input.indexOf(':', lastPos + 1);
+ }
+ if (lastPos > 0)
+ result.append(input.substring(lastPos));
+
+ return result.toString();
+ }
}
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.utils;
-
-import android.app.Application;
-import android.content.res.Resources;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.util.Log;
-
-import androidx.lifecycle.MutableLiveData;
-
-import net.ktnx.mobileledger.BuildConfig;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class MobileLedgerDatabase extends SQLiteOpenHelper {
- public static final MutableLiveData<Boolean> initComplete = new MutableLiveData<>(false);
- private static final String DB_NAME = "MoLe.db";
- private static final int LATEST_REVISION = 41;
- private static final String CREATE_DB_SQL = "create_db";
- private final Application mContext;
-
- public MobileLedgerDatabase(Application context) {
- super(context, DB_NAME, null, LATEST_REVISION);
- debug("db", "creating helper instance");
- mContext = context;
- super.setWriteAheadLoggingEnabled(true);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- debug("db", "onCreate called");
- applyRevisionFile(db, CREATE_DB_SQL);
- }
-
- @Override
- public void onConfigure(SQLiteDatabase db) {
- super.onConfigure(db);
- db.execSQL("pragma case_sensitive_like=ON;");
- if (BuildConfig.DEBUG)
- db.execSQL("PRAGMA foreign_keys=ON");
- }
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- debug("db",
- String.format(Locale.US, "needs upgrade from version %d to version %d", oldVersion,
- newVersion));
- for (int i = oldVersion + 1; i <= newVersion; i++)
- applyRevision(db, i);
- }
- private void applyRevision(SQLiteDatabase db, int rev_no) {
- String rev_file = String.format(Locale.US, "sql_%d", rev_no);
-
- applyRevisionFile(db, rev_file);
- }
- private void applyRevisionFile(SQLiteDatabase db, String rev_file) {
- final Resources rm = mContext.getResources();
- int res_id = rm.getIdentifier(rev_file, "raw", mContext.getPackageName());
- if (res_id == 0)
- throw new SQLException(String.format(Locale.US, "No resource for %s", rev_file));
- db.beginTransaction();
- try (InputStream res = rm.openRawResource(res_id)) {
- debug("db", "Applying " + rev_file);
- InputStreamReader isr = new InputStreamReader(res);
- BufferedReader reader = new BufferedReader(isr);
-
- String line;
- int line_no = 1;
- while ((line = reader.readLine()) != null) {
- if (line.startsWith("--")) {
- line_no++;
- continue;
- }
- if (line.isEmpty()) {
- line_no++;
- continue;
- }
- try {
- db.execSQL(line);
- }
- catch (Exception e) {
- throw new RuntimeException(
- String.format("Error applying %s, line %d", rev_file, line_no), e);
- }
- line_no++;
- }
-
- db.setTransactionSuccessful();
- }
- catch (IOException e) {
- Log.e("db", String.format("Error opening raw resource for %s", rev_file));
- e.printStackTrace();
- }
- finally {
- db.endTransaction();
- }
- }
-}
import androidx.annotation.NonNull;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.db.Profile;
import org.jetbrains.annotations.NotNull;
public final class NetworkUtil {
private static final int thirtySeconds = 30000;
@NotNull
- public static HttpURLConnection prepareConnection(@NonNull MobileLedgerProfile profile,
+ public static HttpURLConnection prepareConnection(@NonNull Profile profile,
@NonNull String path) throws IOException {
- return prepareConnection(profile.getUrl(), path, profile.isAuthEnabled());
+ return prepareConnection(profile.getUrl(), path, profile.useAuthentication());
}
public static HttpURLConnection prepareConnection(@NonNull String url, @NonNull String path,
boolean authEnabled) throws IOException {
--- /dev/null
+/*
+ * Copyright © 2021 Damyan Ivanov.
+ * This file is part of MoLe.
+ * MoLe is free software: you can distribute it and/or modify it
+ * under the term of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your opinion), any later version.
+ *
+ * MoLe is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License terms for details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package net.ktnx.mobileledger.utils;
+
+import net.ktnx.mobileledger.BuildConfig;
+
+import java.util.Locale;
+
+public class Profiler {
+ private final String name;
+ private long opStart = 0;
+ private long opCount = 0;
+ private long opMills = 0;
+ public Profiler(String name) {
+ this.name = name;
+ }
+ public void opStart() {
+ if (!BuildConfig.DEBUG)
+ return;
+
+ if (opStart != 0)
+ throw new IllegalStateException("opStart() already called with no opEnd()");
+ this.opStart = System.currentTimeMillis();
+ opCount++;
+ }
+ public void opEnd() {
+ if (!BuildConfig.DEBUG)
+ return;
+
+ if (opStart == 0)
+ throw new IllegalStateException("opStart() not called");
+ opMills += System.currentTimeMillis() - opStart;
+ opStart = 0;
+ }
+ public void dumpStats() {
+ if (!BuildConfig.DEBUG)
+ return;
+
+ Logger.debug("profiler", String.format(Locale.ROOT,
+ "Operation '%s' executed %d times for %d ms. Average time %4.2fms", name, opCount,
+ opMills, 1.0 * opMills / opCount));
+ }
+}
import java.util.Calendar;
import java.util.Date;
+import java.util.Locale;
public class SimpleDate implements Comparable<SimpleDate> {
public final int year;
calendar.set(year, month, day);
return calendar;
}
+ public String toString() {
+ return String.format(Locale.US, "%d-%02d-%02d", year, month, day);
+ }
}
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
- android:animation="@anim/slide_in_right"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator" />
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime"
- android:fillAfter="true">
- <rotate
- android:fromDegrees="0"
- android:pivotX="50%"
- android:pivotY="50%"
- android:toDegrees="180" />
-</set>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime"
- android:fillAfter="true">
- <rotate
- android:fromDegrees="180"
- android:pivotX="50%"
- android:pivotY="50%"
- android:toDegrees="0" />
-</set>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime"
- android:fillAfter="true"
- android:fillEnabled="true">
- <scale
- android:fillAfter="false"
- android:fromYScale="0.0"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator"
- android:toYScale="1.0"
- android:fromXScale="1.0"
- android:toXScale="1.0"/>
-</set>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime">
- <translate
- android:fromXDelta="100%"
- android:toXDelta="0%" />
-</set>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime">
- <translate
- android:fromXDelta="0%"
- android:toXDelta="100%" />
- <alpha
- android:fromAlpha="1.0"
- android:toAlpha="0.0" />
-</set>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<set xmlns:android="http://schemas.android.com/apk/res/android"
- android:duration="@android:integer/config_shortAnimTime"
- android:fillAfter="true"
- android:fillEnabled="true">
- <scale
- android:fillAfter="false"
- android:fromYScale="1.0"
- android:interpolator="@android:anim/accelerate_decelerate_interpolator"
- android:toYScale="0.0"
- android:fromXScale="1.0"
- android:toXScale="1.0"/>
-</set>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
- <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+ <foreground android:drawable="@drawable/launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
- <foreground android:drawable="@drawable/ic_launcher_foreground"/>
+ <foreground android:drawable="@drawable/launcher_foreground" />
</adaptive-icon>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:autoMirrored="true"
- android:viewportWidth="90"
- android:viewportHeight="90"
- >
- <path
- android:fillAlpha="1"
- android:fillColor="#935FF2"
- android:fillType="nonZero"
- android:pathData="M0,0 l0,90 l90,0 l0,-90 z"
- android:strokeWidth="0"
- android:strokeAlpha="1"
- android:strokeColor="#00000000"
- android:strokeLineCap="square"
- android:strokeLineJoin="round"
- />
-</vector>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@drawable/ic_star_border_black_24dp"
- android:state_checked="false"/>
- <item android:drawable="@drawable/ic_star_black_24dp"
- android:state_checked="true"/>
- <item android:drawable="@drawable/ic_star_border_black_24dp" />
-</selector>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@drawable/ic_star_border_white_24dp"
- android:state_checked="false"/>
- <item android:drawable="@drawable/ic_star_white_24dp"
- android:state_checked="true"/>
- <item android:drawable="@drawable/ic_star_border_white_24dp" />
-</selector>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="line"
- android:thickness="1dp"
- android:tint="?colorSecondary"
- >
- <stroke
- android:width="1dp"
- android:color="?colorSecondary"
- android:dashWidth="2dp"
- android:dashGap="6dp"
- />
-
-</shape>
\ No newline at end of file
+++ /dev/null
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?colorSecondary"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:name="p"
- android:fillColor="#FF000000"
- android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
-</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M14.06,9.94L12,9l2.06,-0.94L15,6l0.94,2.06L18,9l-2.06,0.94L15,12L14.06,9.94zM4,14l0.94,-2.06L7,11l-2.06,-0.94L4,8l-0.94,2.06L1,11l2.06,0.94L4,14zM8.5,9l1.09,-2.41L12,5.5L9.59,4.41L8.5,2L7.41,4.41L5,5.5l2.41,1.09L8.5,9zM4.5,20.5l6,-6.01l4,4L23,8.93l-1.41,-1.41l-7.09,7.97l-4,-4L3,19L4.5,20.5z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4V15z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorOnPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9,3L5,6.99h3L8,14h2L10,6.99h3L9,3zM16,17.01L16,10h-2v7.01h-3L15,21l4,-3.99h-3z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="#EEEEEE"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z"
+ />
+</vector>
--- /dev/null
+<!--
+ ~ Copyright Google Inc.
+ ~
+ ~ Licensed under the Apache License, version 2.0 ("the License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the license at:
+ ~
+ ~ https://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distribution under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~
+ ~ Modified/adapted by Damyan Ivanov for MoLe
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:autoMirrored="true"
+ android:tint="?colorPrimary"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ >
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"
+ />
+</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp" android:tint="#EEEEEE"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector android:height="24dp" android:tint="#EEEEEE"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:autoMirrored="true"
- android:tint="?colorPrimary"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:tint="?colorPrimary"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M17,12h-5v5h5v-5zM16,1v2L8,3L8,1L6,1v2L5,3c-1.11,0 -1.99,0.9 -1.99,2L3,19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2h-1L18,1h-2zM19,19L5,19L5,8h14v11z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="#313131"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp" android:tint="?colorPrimary"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
-</vector>
\ No newline at end of file
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp"
- android:tint="?colorSecondary"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0"
- android:width="24dp"
- xmlns:android="http://schemas.android.com/apk/res/android"
- >
- <path
- android:fillColor="#FF000000"
- android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"
- />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?attr/colorPrimary"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0"
- >
- <path
- android:fillColor="#FF000000"
- android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"
- />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp" android:tint="#313131"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp" android:tint="#313131"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector android:height="24dp" android:tint="#EEEEEE"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector android:height="24dp" android:tint="#EEEEEE"
- android:viewportHeight="24.0" android:viewportWidth="24.0"
- android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
- <path android:fillColor="#FF000000" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-0.25 1.97,-0.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z"
- />
-</vector>
\ No newline at end of file
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="6.35"
- android:viewportHeight="6.3500004">
- <path
- android:pathData="m5.72461,0.0004a0.43254,0.43254 0,0 0,-0.37109 0.23828L2.79688,5.14102 0.94531,2.91837A0.43254,0.43254 0,1 0,0.28125 3.4711l2.26563,2.72266a0.43254,0.43254 0,0 0,0.7168 -0.0762l2.85742,-5.48047a0.43254,0.43254 0,0 0,-0.39648 -0.63672z"
- android:strokeAlpha="1"
- android:strokeLineJoin="round"
- android:strokeWidth="0.86500001"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="evenOdd"
- android:fillAlpha="1"
- android:strokeLineCap="round"/>
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector android:height="24dp"
- android:tint="?colorSecondary"
- android:viewportHeight="24.0"
- android:viewportWidth="24.0"
- android:width="24dp"
- xmlns:android="http://schemas.android.com/apk/res/android"
- >
- <path
- android:fillColor="#FF000000"
- android:pathData="M12,5.83L15.17,9l1.41,-1.41L12,3 7.41,7.59 8.83,9 12,5.83zM12,18.17L8.83,15l-1.41,1.41L12,21l4.59,-4.59L15.17,15 12,18.17z"
- />
-</vector>
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:tint="?colorSecondary"
- android:viewportWidth="24.0"
- android:viewportHeight="24.0">
- <path
- android:fillColor="#FF000000"
- android:pathData="M4,14h4v-4L4,10v4zM4,19h4v-4L4,15v4zM4,9h4L8,5L4,5v4zM9,14h12v-4L9,10v4zM9,19h12v-4L9,15v4zM9,5v4h12L21,5L9,5z" />
-</vector>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
- <gradient
- android:type="linear"
- android:endColor="?colorPrimary"
- android:centerColor="?colorPrimaryTransparent"
- android:startColor="?colorPrimary" />
- <size android:height="1dp" />
-</shape>
\ No newline at end of file
+++ /dev/null
-<!--
- ~ Copyright Google Inc.
- ~
- ~ Licensed under the Apache License, version 2.0 ("the License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the license at:
- ~
- ~ https://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distribution under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- ~
- ~ Modified/adapted by Damyan Ivanov for MoLe
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:autoMirrored="true"
- android:tint="?colorOnPrimary"
- android:viewportWidth="24"
- android:viewportHeight="24"
- >
- <path
- android:fillColor="@android:color/white"
- android:pathData="M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z"
- />
-</vector>
+++ /dev/null
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="108dp"
- android:height="108dp"
- android:viewportWidth="108"
- android:viewportHeight="108">
- <group android:scaleX="0.73"
- android:scaleY="0.73"
- android:translateX="14.58"
- android:translateY="14.58">
- <path
- android:pathData="m42.06,32.596c-1.331,1.331 -1.33,3.49 0.002,4.82 1.331,1.33 3.488,1.329 4.818,-0.002 3.962,-3.962 10.28,-3.962 14.242,0 1.33,1.331 3.487,1.332 4.818,0.002 1.332,-1.33 1.333,-3.488 0.002,-4.82 -7.523,-6.703 -17.464,-6.433 -23.881,0z"
- android:strokeLineJoin="round"
- android:strokeWidth="7.1924"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="nonZero"
- android:strokeLineCap="round"/>
- <path
- android:pathData="m32.531,23.62c-1.33,1.33 -1.33,3.487 0,4.818 1.33,1.33 3.487,1.33 4.818,0 9.225,-9.227 24.078,-9.227 33.304,0 1.33,1.33 3.487,1.33 4.818,0 1.33,-1.33 1.33,-3.487 0,-4.818 -12.952,-12.351 -31.975,-11.302 -42.939,0z"
- android:strokeLineJoin="round"
- android:strokeWidth="7.1924"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="nonZero"
- android:strokeLineCap="round"/>
- <path
- android:pathData="m17.52,41.916c-3.229,0.666 -3.355,3.178 -3.355,3.827L14.165,89.41c0,2.088 1.746,3.837 3.835,3.837 13.048,-2.199 19.956,-2.121 36,0.007C68.79,91.212 79.587,90.998 90,93.247c2.089,0 3.835,-1.749 3.835,-3.837L93.835,45.743c0,-2.094 -1.814,-3.538 -3.636,-3.827 -9.491,-1.502 -13.338,-2.216 -27.678,-0.945 0.484,1.475 0.72,2.785 0.442,4.184 10.721,-1.146 19.131,-0.668 26.469,0.915l-0.045,42.916C77.902,86.92 70.894,86.96 56.213,88.799L56.213,52.774c-1.422,0.348 -3.104,0.332 -4.426,0L51.787,88.799C40.094,86.929 28.873,86.93 18.591,88.987L18.591,46.07c11.035,-1.397 15.892,-2.046 26.444,-0.915 -0.18,-1.441 0.022,-2.978 0.451,-4.184 -9.086,-1.124 -17.014,-0.646 -27.966,0.945z"
- android:strokeLineJoin="round"
- android:strokeWidth="4.42609"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="nonZero"
- android:strokeLineCap="round"/>
- <path
- android:pathData="M32.773,55.429L32.773,65.965L22.238,65.965v5.137h10.534v10.534h5.137L37.91,71.102L48.446,71.102L48.446,65.965L37.91,65.965L37.91,55.429Z"
- android:strokeLineJoin="miter"
- android:strokeWidth="5.42196"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="evenOdd"
- android:strokeLineCap="butt"/>
- <path
- android:pathData="M59.352,65.965L59.352,71.102L85.559,71.102v-5.137z"
- android:strokeLineJoin="miter"
- android:strokeWidth="5.42196"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="evenOdd"
- android:strokeLineCap="butt"/>
- <path
- android:pathData="m54,40.578c1.764,0 3.407,1.645 3.407,3.407 0,1.762 -1.643,3.403 -3.407,3.403 -1.764,0 -3.407,-1.641 -3.407,-3.403 0,-1.762 1.643,-3.407 3.407,-3.407z"
- android:strokeLineJoin="round"
- android:strokeWidth="11.5078"
- android:fillColor="#ffffff"
- android:strokeColor="#00000000"
- android:fillType="nonZero"
- android:strokeLineCap="round"/>
- </group>
-</vector>
--- /dev/null
+<!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108"
+ >
+ <group
+ android:scaleX="0.73"
+ android:scaleY="0.73"
+ android:translateX="14.58"
+ android:translateY="14.58"
+ >
+ <path
+ android:pathData="m42.06,32.596c-1.331,1.331 -1.33,3.49 0.002,4.82 1.331,1.33 3.488,1.329 4.818,-0.002 3.962,-3.962 10.28,-3.962 14.242,0 1.33,1.331 3.487,1.332 4.818,0.002 1.332,-1.33 1.333,-3.488 0.002,-4.82 -7.523,-6.703 -17.464,-6.433 -23.881,0z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="7.1924"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="m32.531,23.62c-1.33,1.33 -1.33,3.487 0,4.818 1.33,1.33 3.487,1.33 4.818,0 9.225,-9.227 24.078,-9.227 33.304,0 1.33,1.33 3.487,1.33 4.818,0 1.33,-1.33 1.33,-3.487 0,-4.818 -12.952,-12.351 -31.975,-11.302 -42.939,0z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="7.1924"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="m17.52,41.916c-3.229,0.666 -3.355,3.178 -3.355,3.827L14.165,89.41c0,2.088 1.746,3.837 3.835,3.837 13.048,-2.199 19.956,-2.121 36,0.007C68.79,91.212 79.587,90.998 90,93.247c2.089,0 3.835,-1.749 3.835,-3.837L93.835,45.743c0,-2.094 -1.814,-3.538 -3.636,-3.827 -9.491,-1.502 -13.338,-2.216 -27.678,-0.945 0.484,1.475 0.72,2.785 0.442,4.184 10.721,-1.146 19.131,-0.668 26.469,0.915l-0.045,42.916C77.902,86.92 70.894,86.96 56.213,88.799L56.213,52.774c-1.422,0.348 -3.104,0.332 -4.426,0L51.787,88.799C40.094,86.929 28.873,86.93 18.591,88.987L18.591,46.07c11.035,-1.397 15.892,-2.046 26.444,-0.915 -0.18,-1.441 0.022,-2.978 0.451,-4.184 -9.086,-1.124 -17.014,-0.646 -27.966,0.945z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="4.42609"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:strokeLineCap="round"/>
+ <path
+ android:pathData="M32.773,55.429L32.773,65.965L22.238,65.965v5.137h10.534v10.534h5.137L37.91,71.102L48.446,71.102L48.446,65.965L37.91,65.965L37.91,55.429Z"
+ android:strokeLineJoin="miter"
+ android:strokeWidth="5.42196"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="evenOdd"
+ android:strokeLineCap="butt"/>
+ <path
+ android:pathData="M59.352,65.965L59.352,71.102L85.559,71.102v-5.137z"
+ android:strokeLineJoin="miter"
+ android:strokeWidth="5.42196"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="evenOdd"
+ android:strokeLineCap="butt"/>
+ <path
+ android:pathData="m54,40.578c1.764,0 3.407,1.645 3.407,3.407 0,1.762 -1.643,3.403 -3.407,3.403 -1.764,0 -3.407,-1.641 -3.407,-3.403 0,-1.762 1.643,-3.407 3.407,-3.407z"
+ android:strokeLineJoin="round"
+ android:strokeWidth="11.5078"
+ android:fillColor="#ffffff"
+ android:strokeColor="#00000000"
+ android:fillType="nonZero"
+ android:strokeLineCap="round"/>
+ </group>
+</vector>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/thumb_row_height"
+ android:padding="@dimen/half_text_margin"
+ >
+
+ <TextView
+ android:id="@+id/account_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="TextView with a very long account name. a really long account name that needs to one\non two lines"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
+ />
+ <TextView
+ android:id="@+id/amounts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/text_margin"
+ android:gravity="end"
+ android:text="LongCurrencyName 1 234 567,89"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
+ android:textColor="?commentColor"
+ app:layout_goneMarginStart="0dp"
+ />
+ <androidx.constraintlayout.helper.widget.Flow
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:constraint_referenced_ids="account_name,amounts"
+ app:flow_firstHorizontalBias="0"
+ app:flow_firstHorizontalStyle="spread_inside"
+ app:flow_horizontalBias="1"
+ app:flow_verticalStyle="spread_inside"
+ app:flow_wrapMode="chain"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/account_summary_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:longClickable="true"
+ android:minHeight="@dimen/default_account_row_height"
+ >
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/account_name_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintHorizontal_chainStyle="spread_inside"
+
+ >
+ <TextView
+ android:id="@+id/account_row_acc_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:gravity="center_vertical"
+ android:longClickable="true"
+ android:paddingStart="8dp"
+ android:text="Example AccountName That Is Too Long And Has to Be Wrapped On More Than One Line Words Words Words"
+ android:textAppearance="@android:style/TextAppearance.Material.Medium"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/account_expander_container"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/account_expander_container"
+ android:layout_width="@dimen/thumb_row_height"
+ android:layout_height="@dimen/default_account_row_height"
+ android:foregroundGravity="center_vertical"
+ android:minHeight="@dimen/default_account_row_height"
+ app:layout_constraintBottom_toBottomOf="@id/account_row_acc_name"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/account_row_acc_name"
+ app:layout_constraintTop_toTopOf="@id/account_row_acc_name"
+ >
+
+ <ImageView
+ android:id="@+id/account_expander"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:background="@drawable/ic_expand_less_black_24dp"
+ android:backgroundTint="?colorPrimary"
+ android:clickable="true"
+ android:contentDescription="@string/sub_accounts_expand_collapse_trigger_description"
+ android:focusable="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ <TextView
+ android:id="@+id/account_row_acc_amounts"
+ style="@style/account_summary_amounts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="12dp"
+ android:gravity="center_vertical"
+ android:text="USD 123,45\n678,90\nIRAUSD -17 000.00"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintWidth_min="90sp"
+ tools:ignore="HardcodedText"
+ />
+
+ <FrameLayout
+ android:id="@+id/account_row_amounts_expander_container"
+ android:layout_width="0dp"
+ android:layout_height="18sp"
+ android:background="@drawable/fade_down_white"
+ app:layout_constraintBottom_toBottomOf="@id/account_row_acc_amounts"
+ app:layout_constraintEnd_toEndOf="@id/account_row_acc_amounts"
+ app:layout_constraintStart_toStartOf="@id/account_row_acc_amounts"
+ />
+ <androidx.constraintlayout.helper.widget.Flow
+ android:id="@+id/flow_wrapper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/default_account_row_height"
+ app:constraint_referenced_ids="account_name_layout,account_row_acc_amounts"
+ app:flow_firstHorizontalBias="0"
+ app:flow_firstHorizontalStyle="spread_inside"
+ app:flow_horizontalBias="1"
+ app:flow_verticalStyle="spread_inside"
+ app:flow_wrapMode="chain"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:layout_marginEnd="@dimen/half_text_margin"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>
+
+
+<!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/account_summary_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:longClickable="true"
+ android:paddingTop="4dp"
+ >
+
+ <TextView
+ android:id="@+id/last_update_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
+ android:layout_weight="1"
+ android:gravity="center"
+ android:text="1 123 transactions as of 29.02.2020 13:37"
+ android:textAppearance="@android:style/TextAppearance.Material.Small"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-
-
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/account_summary_row"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true"
- android:longClickable="true"
- >
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/account_name_layout"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toStartOf="@+id/account_row_acc_amounts"
- app:layout_constraintHorizontal_chainStyle="spread_inside"
- app:layout_constraintHorizontal_weight="3"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- >
- <TextView
- android:id="@+id/account_row_acc_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:gravity="center_vertical"
- android:longClickable="true"
- android:minHeight="@dimen/default_account_row_height"
- android:paddingStart="8dp"
- android:text="AccountName"
- android:textAppearance="@android:style/TextAppearance.Material.Medium"
- app:layout_constrainedWidth="true"
- app:layout_constraintEnd_toStartOf="@id/account_expander_container"
- app:layout_constraintHorizontal_bias="0.0"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_max="wrap"
- app:layout_constraintWidth_percent=".67"
- tools:ignore="HardcodedText"
- />
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/account_expander_container"
- android:layout_width="@dimen/thumb_row_height"
- android:layout_height="@dimen/default_account_row_height"
- android:foregroundGravity="center_vertical"
- android:minHeight="@dimen/default_account_row_height"
- app:layout_constraintBottom_toBottomOf="@id/account_row_acc_name"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@id/account_row_acc_name"
- app:layout_constraintTop_toTopOf="@id/account_row_acc_name"
- >
-
- <ImageView
- android:id="@+id/account_expander"
- android:layout_width="32dp"
- android:layout_height="32dp"
- android:background="@drawable/ic_expand_less_black_24dp"
- android:backgroundTint="?colorPrimary"
- android:clickable="true"
- android:contentDescription="@string/sub_accounts_expand_collapse_trigger_description"
- android:focusable="true"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- />
- </androidx.constraintlayout.widget.ConstraintLayout>
- </androidx.constraintlayout.widget.ConstraintLayout>
- <TextView
- android:id="@+id/account_row_acc_amounts"
- style="@style/account_summary_amounts"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginStart="12dp"
- android:layout_marginEnd="8dp"
- android:gravity="center_vertical"
- android:minHeight="@dimen/default_account_row_height"
- android:text="USD 123,45\n678,90\nIRAUSD -17 000.00"
- android:textAppearance="@style/TextAppearance.AppCompat.Medium"
- app:layout_constrainedWidth="true"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_weight="2"
- app:layout_constraintStart_toEndOf="@id/account_name_layout"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_min="90sp"
- tools:ignore="HardcodedText"
- />
-
- <FrameLayout
- android:id="@+id/account_row_amounts_expander_container"
- android:layout_width="0dp"
- android:layout_height="18sp"
- android:background="@drawable/fade_down_white"
- app:layout_constraintBottom_toBottomOf="@id/account_row_acc_amounts"
- app:layout_constraintEnd_toEndOf="@id/account_row_acc_amounts"
- app:layout_constraintStart_toStartOf="@id/account_row_acc_amounts"
- >
-
- </FrameLayout>
- <include layout="@layout/last_update_layout" />
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_height="match_parent"
+ tools:context=".ui.activity.MainActivity"
+ >
- <include
- layout="@layout/no_profiles"
- android:visibility="gone" />
+ <ScrollView
+ android:id="@+id/no_profiles_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?table_row_dark_bg"
+ android:visibility="visible"
+ >
+ <androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
- <include
- layout="@layout/main_app_layout"
- android:visibility="gone" />
+ <FrameLayout
+ android:id="@+id/welcome_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+
+ <include layout="@layout/nav_header_layout" />
+ </FrameLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/activity_horizontal_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/welcome_header"
+ >
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="48dp"
+ android:text="@string/text_welcome"
+ android:textColor="?textColor"
+ android:textSize="48sp"
+ app:layout_constraintBottom_toTopOf="@id/textView3"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <TextView
+ android:id="@+id/textView3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="24dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:text="@string/text_welcome_profile_needed"
+ android:textColor="?textColor"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toTopOf="@id/btn_no_profiles_add"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/textView"
+ />
+
+ <Button
+ android:id="@+id/btn_no_profiles_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="24dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="8dp"
+ android:backgroundTint="?colorSecondary"
+ android:drawablePadding="16dp"
+ android:text="@string/create_profile_label"
+ android:textColor="@color/design_default_color_on_primary"
+ app:layout_constraintBottom_toTopOf="@id/restore_hint"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/textView3"
+ />
+ <TextView
+ android:id="@+id/restore_hint"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp"
+ android:layout_marginVertical="24dp"
+ android:text="@string/no_profile_restore_hint"
+ android:textColor="?textColor"
+ android:textSize="20sp"
+ app:layout_constraintBottom_toTopOf="@id/btn_restore"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/btn_no_profiles_add"
+ />
+
+ <Button
+ android:id="@+id/btn_restore"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="8dp"
+ android:layout_marginVertical="24dp"
+ android:drawableStart="@drawable/ic_baseline_restore_24"
+ android:drawablePadding="@dimen/text_margin"
+ android:text="@string/restore_button_label"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/restore_hint"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+ <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/main_app_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/colorBackground"
+ android:orientation="vertical"
+ android:visibility="gone"
+ >
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/btn_add_transaction"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_margin="@dimen/fab_margin"
+ android:contentDescription="@string/new_transaction_fab_description"
+ app:backgroundTint="?colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:maxImageSize="36dp"
+ app:srcCompat="@drawable/ic_add_white_24dp"
+ />
+
+ <androidx.drawerlayout.widget.DrawerLayout
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:openDrawer="start"
+ >
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/pager_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ >
+
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?colorPrimary"
+ android:theme="@style/AppTheme.AppBarOverlay"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:popupTheme="@style/AppTheme.PopupOverlay"
+ app:subtitleTextAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle"
+ app:titleTextAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
+ />
+
+
+ <LinearLayout
+ android:id="@+id/transaction_progress_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/toolbar"
+ >
+
+ <ProgressBar
+ android:id="@+id/transaction_list_progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-8dp"
+ android:layout_marginBottom="-7dp"
+ android:layout_weight="1"
+ android:indeterminate="true"
+ android:min="0"
+ android:padding="0dp"
+ android:progressTint="?colorPrimary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+
+ <TextView
+ android:id="@+id/transaction_list_cancel_download"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_clear_accent_24dp"
+ android:clickable="true"
+ android:focusable="true"
+ />
+ </LinearLayout>
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/main_pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_progress_layout"
+ >
+
+ </androidx.viewpager2.widget.ViewPager2>
+
+ <View
+ android:layout_width="0dp"
+ android:layout_height="?attr/main_header_shadow_height"
+ android:background="@drawable/drop_shadow"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_progress_layout"
+ />
+
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <com.google.android.material.navigation.NavigationView xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/nav_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:fitsSystemWindows="true"
+ >
+
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="0dp"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ >
+
+ <LinearLayout
+ android:id="@+id/nav_fixed_items"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="@drawable/list_divider"
+ android:elevation="2dp"
+ android:orientation="vertical"
+ android:showDividers="beginning"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="parent"
+ >
+
+ <TextView
+ android:id="@+id/textView2"
+ style="@style/nav_button"
+ android:layout_weight="1"
+ android:text="@string/action_settings"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/ic_settings_black_24dp"
+ />
+ <TextView
+ android:id="@+id/nav_backup_restore"
+ style="@style/nav_button"
+ android:layout_weight="1"
+ android:text="@string/action_import_export"
+ app:drawableStartCompat="@drawable/ic_baseline_backup_24"
+ />
+
+ </LinearLayout>
+
+ <ScrollView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/nav_fixed_items"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+
+ <LinearLayout
+ android:id="@+id/nav_upper"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:orientation="vertical"
+ android:showDividers="beginning"
+ app:layout_constraintBottom_toTopOf="@+id/nav_fixed_items"
+ app:layout_constraintTop_toBottomOf="@+id/nav_header"
+ >
+
+ <include layout="@layout/nav_header_layout" />
+
+ <LinearLayout
+ android:id="@+id/nav_actions"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ >
+
+ <TextView
+ android:id="@+id/nav_account_summary"
+ style="@style/nav_button"
+ android:text="@string/account_summary_title"
+ app:drawableStartCompat="@drawable/ic_home_black_24dp"
+ />
+
+ <TextView
+ android:id="@+id/nav_latest_transactions"
+ style="@style/nav_button"
+ android:text="@string/nav_transactions_title"
+ app:drawableStartCompat="@drawable/ic_event_note_black_24dp"
+ />
+
+ <TextView
+ android:id="@+id/textView5"
+ style="@style/nav_button"
+ android:text="@string/nav_reports_title"
+ android:visibility="gone"
+ app:drawableStartCompat="@drawable/ic_assignment_black_24dp"
+ />
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/nav_profile_list_head_layout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/thumb_row_height"
+ >
+
+ <ImageView
+ android:id="@+id/nav_new_profile_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:contentDescription="@string/icon"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/nav_profile_list_head_buttons"
+ app:layout_constraintStart_toEndOf="@id/nav_profiles_label"
+ app:layout_constraintTop_toTopOf="parent"
+ app:srcCompat="@drawable/ic_add_circle_white_24dp"
+ />
+
+ <LinearLayout
+ android:id="@+id/nav_profile_list_head_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+
+ <ImageView
+ android:id="@+id/nav_profiles_cancel_edit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_clear_accent_24dp"
+ android:contentDescription="@string/icon"
+ android:gravity="end|center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <ImageView
+ android:id="@+id/nav_profiles_start_edit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_settings_black_24dp"
+ android:contentDescription="@string/icon"
+ android:gravity="end|center_vertical"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/nav_profiles_label"
+ style="@style/nav_button"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:gravity="start|center_vertical"
+ android:text="@string/profiles"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ <LinearLayout
+ android:id="@+id/nav_profile_list_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:nestedScrollingEnabled="false"
+ android:orientation="vertical"
+ >
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/nav_profile_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:isScrollContainer="false"
+ android:nestedScrollingEnabled="false"
+ android:orientation="vertical"
+ >
+
+ </androidx.recyclerview.widget.RecyclerView>
+
+ </LinearLayout>
+
+ </LinearLayout>
+ <TextView
+ android:id="@+id/nav_patterns"
+ style="@style/nav_button"
+ android:text="@string/nav_templates"
+ app:drawableStartCompat="@drawable/ic_baseline_auto_graph_24"
+ />
+
+ </LinearLayout>
+ </ScrollView>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ </com.google.android.material.navigation.NavigationView>
+ </androidx.drawerlayout.widget.DrawerLayout>
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.activity.NewTransactionActivity"
+ tools:context=".ui.new_transaction.NewTransactionActivity"
+ android:fitsSystemWindows="false"
>
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
- android:layout_height="@dimen/toolbar_height"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
+ app:subtitleTextAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle"
+ app:titleTextAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
/>
</com.google.android.material.appbar.AppBarLayout>
/>
</androidx.constraintlayout.widget.ConstraintLayout>
-
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fabAdd"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/fab_margin"
+ android:layout_marginBottom="@dimen/fab_margin"
+ android:contentDescription="@string/add_button_description"
+ android:padding="@dimen/fab_margin"
+ android:tint="?android:attr/colorBackground"
+ android:visibility="visible"
+ app:backgroundTint="?colorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:srcCompat="@drawable/ic_save_white_24dp"
+ />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".ui.activity.ProfileDetailActivity">
+ tools:context=".ui.profiles.ProfileDetailActivity"
+ android:fitsSystemWindows="false"
+ >
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
- android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ >
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/fab"
+ android:id="@+id/fabAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ui.templates.TemplatesActivity"
+ >
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:theme="@style/AppTheme.AppBarOverlay"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorPrimary"
+ app:layout_collapseMode="pin"
+ app:popupTheme="@style/AppTheme.PopupOverlay"
+ />
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.fragment.app.FragmentContainerView
+ android:id="@+id/fragment_container"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/appbar"
+ app:navGraph="@navigation/template_list_navigation"
+ />
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/fab"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/fab_margin"
+ android:contentDescription="@string/add_button_description"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:srcCompat="@drawable/ic_add_white_24dp"
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".BackupsActivity"
+ >
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ android:theme="@style/AppTheme.AppBarOverlay"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ >
+ <androidx.appcompat.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorPrimary"
+ app:layout_collapseMode="pin"
+ app:popupTheme="@style/AppTheme.PopupOverlay"
+ />
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <ScrollView
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/appbar"
+ >
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/activity_horizontal_margin"
+ >
+ <TextView
+ android:id="@+id/backup_header"
+ style="@style/TextAppearance.MaterialComponents.Headline4"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/backup_header"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ <TextView
+ android:id="@+id/backup_explanation_text"
+ style="@style/TextAppearance.MaterialComponents.Body1"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/backup_explanation"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/backup_header"
+ />
+ <TextView
+ android:id="@+id/backup_button"
+ style="@style/TextAppearance.MaterialComponents.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="@dimen/text_margin"
+ android:drawablePadding="@dimen/text_margin"
+ android:gravity="center_vertical"
+ android:text="@string/backup_button_label"
+ app:drawableStartCompat="@drawable/ic_baseline_backup_24"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/backup_explanation_text"
+ />
+ <TextView
+ android:id="@+id/restore_header"
+ style="@style/TextAppearance.MaterialComponents.Headline4"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/restore_header"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/backup_button"
+ />
+ <TextView
+ android:id="@+id/restore_explanation_text"
+ style="@style/TextAppearance.MaterialComponents.Body1"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/restore_explanation"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/restore_header"
+ />
+ <TextView
+ android:id="@+id/restore_button"
+ style="@style/TextAppearance.MaterialComponents.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="@dimen/text_margin"
+ android:drawablePadding="@dimen/text_margin"
+ android:gravity="center_vertical"
+ android:text="@string/restore_button_label"
+ app:drawableStartCompat="@drawable/ic_baseline_restore_24"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/restore_explanation_text"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+ </ScrollView>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:minWidth="60dp"
android:animateLayoutChanges="true"
+ android:minWidth="60dp"
+ android:padding="@dimen/text_margin"
app:layout_constraintWidth_min="60dp"
- android:padding="@dimen/text_margin">
+ >
<com.google.android.material.textview.MaterialTextView
android:id="@+id/label"
android:layout_marginBottom="@dimen/text_margin"
android:text="@string/choose_currency_label"
android:textSize="18sp"
+ app:layout_constraintBottom_toTopOf="@id/list"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:name="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="200dp"
+ android:minHeight="100dp"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
app:layoutManager="LinearLayoutManager"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label"
tools:context="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
- tools:listitem="@layout/fragment_currency_selector">
+ tools:listitem="@layout/fragment_currency_selector"
+ >
</androidx.recyclerview.widget.RecyclerView>
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:layout_marginRight="@dimen/activity_horizontal_margin"
- app:layout_constraintTop_toBottomOf="@id/list"
app:layout_constraintBottom_toTopOf="@id/params_panel"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent">
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/list"
+ >
<TextView
android:id="@+id/btn_add_new"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="@string/add_button"
android:layout_margin="@dimen/text_margin"
+ android:text="@string/add_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_no_currency"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ />
<TextView
+ android:id="@+id/btn_no_currency"
+ style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:id="@+id/btn_no_currency"
+ android:layout_margin="@dimen/text_margin"
android:text="@string/btn_no_currency"
- style="@style/Widget.MaterialComponents.Button.TextButton"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_add_new"
- android:layout_margin="@dimen/text_margin"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
<EditText
android:id="@+id/new_currency_name"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
+ app:layout_constraintTop_toTopOf="parent"
+ android:autofillHints="currency"
+ />
<TextView
android:id="@+id/btn_add_currency"
android:text="@string/add_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/new_currency_name" />
+ app:layout_constraintTop_toBottomOf="@id/new_currency_name"
+ />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/params_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:id="@+id/params_panel"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintTop_toBottomOf="@id/new_currency_panel">
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/new_currency_panel"
+ >
<RadioGroup
android:id="@+id/position_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
+ android:layout_marginBottom="@dimen/text_margin"
android:orientation="horizontal"
app:layout_constraintBottom_toTopOf="@id/currency_gap"
- android:layout_marginBottom="@dimen/text_margin"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="@string/currency_position_left" />
+ android:text="@string/currency_position_left"
+ />
<RadioButton
android:id="@+id/currency_position_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
- android:text="@string/currency_position_right" />
+ android:text="@string/currency_position_right"
+ />
</RadioGroup>
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/currency_gap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/currency_has_gap"
- app:layout_constraintTop_toBottomOf="@id/position_radio_group"
- app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/position_radio_group"
+ />
</androidx.constraintlayout.widget.ConstraintLayout>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
- tools:context="net.ktnx.mobileledger.ui.activity.NewTransactionActivity">
+ tools:context="net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity"
+ >
<ProgressBar
android:id="@+id/progressBar"
</androidx.constraintlayout.widget.ConstraintLayout>
- <com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/fab"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom|end"
- android:layout_marginEnd="@dimen/fab_margin"
- android:layout_marginBottom="@dimen/fab_margin"
- android:padding="@dimen/fab_margin"
- android:tint="?android:attr/colorBackground"
- android:visibility="visible"
- app:backgroundTint="?colorSecondary"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:srcCompat="@drawable/ic_save_white_24dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
android:indeterminate="true"
android:indeterminateTint="?colorPrimary"
android:progressTint="?colorPrimary"
- app:growMode="incoming"
app:layout_constraintBottom_toBottomOf="@id/textView4"
app:layout_constraintEnd_toStartOf="@id/textView4"
app:layout_constraintTop_toTopOf="@id/textView4"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:longClickable="false"
+ >
+
+ <TextView
+ android:id="@+id/group_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:gravity="end"
+ android:minWidth="20sp"
+ android:text="1"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+ <TextView
+ android:id="@+id/colon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text=":"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintStart_toEndOf="@id/group_number"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="HardcodedText"
+ />
+ <TextView
+ android:id="@+id/matched_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/colon"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:animateLayoutChanges="true"
+ android:minWidth="60dp"
+ android:padding="@dimen/text_margin"
+ app:layout_constraintWidth_min="60dp"
+ >
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:text="@string/choose_template_detail_source_label"
+ android:textSize="18sp"
+ app:layout_constraintBottom_toTopOf="@id/list"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list"
+ android:name="net.ktnx.mobileledger.ui.PatternDetailSourceSelectorFragment"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintWidth_min="50dp"
+ app:layout_constraintHeight_min="150dp"
+ android:layout_marginLeft="@dimen/activity_horizontal_margin"
+ android:layout_marginRight="@dimen/activity_horizontal_margin"
+ android:minHeight="100dp"
+ app:layoutManager="LinearLayoutManager"
+ app:layout_constraintBottom_toTopOf="@id/template_error"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/label"
+ tools:context="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
+ tools:listitem="@layout/fragment_template_detail_source_selector"
+ />
+ <TextView
+ android:id="@+id/template_error"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toTopOf="@id/literal_button"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/list"
+ />
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/literal_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/template_details_source_literal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/template_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ui.templates.TemplatesActivity"
+ />
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:id="@+id/main_app_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/colorBackground"
- android:orientation="vertical"
- tools:context=".ui.activity.MainActivity">
-
-
- <com.google.android.material.floatingactionbutton.FloatingActionButton
- android:id="@+id/btn_add_transaction"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom|end"
- android:layout_margin="@dimen/fab_margin"
- app:backgroundTint="?colorSecondary"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:maxImageSize="36dp"
- app:srcCompat="@drawable/ic_add_white_24dp"
- />
-
- <androidx.drawerlayout.widget.DrawerLayout
- android:id="@+id/drawer_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:openDrawer="start">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/pager_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <androidx.appcompat.widget.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="@dimen/toolbar_height"
- android:background="?colorPrimary"
- android:theme="@style/AppTheme.AppBarOverlay"
- app:popupTheme="@style/AppTheme.PopupOverlay"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"/>
-
- <LinearLayout
- android:id="@+id/main_header"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/toolbar">
-
- <LinearLayout
- android:id="@+id/transaction_progress_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:visibility="gone">
-
- <ProgressBar
- android:id="@+id/transaction_list_progress_bar"
- style="?android:attr/progressBarStyleHorizontal"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="-8dp"
- android:layout_marginBottom="-7dp"
- android:layout_weight="1"
- android:indeterminate="true"
- android:padding="0dp"
- android:min="0"
- android:progressTint="?colorPrimary"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent" />
-
- <TextView
- android:id="@+id/transaction_list_cancel_download"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/ic_clear_accent_24dp"
- android:clickable="true"
- android:focusable="true"
- android:onClick="onStopTransactionRefreshClick" />
- </LinearLayout>
-
- </LinearLayout>
-
- <androidx.viewpager.widget.ViewPager
- android:id="@+id/root_frame"
- android:layout_width="match_parent"
- android:layout_height="0dp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/main_header">
-
- </androidx.viewpager.widget.ViewPager>
-
- <View
- android:layout_width="0dp"
- android:layout_height="?attr/main_header_shadow_height"
- android:background="@drawable/drop_shadow"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/main_header" />
-
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- <include layout="@layout/main_navigation" />
-
- </androidx.drawerlayout.widget.DrawerLayout>
-</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<com.google.android.material.navigation.NavigationView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/nav_view"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_gravity="start"
- android:fitsSystemWindows="true"
- >
-
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:layout_marginBottom="0dp"
- android:animateLayoutChanges="true"
- android:orientation="vertical">
-
- <LinearLayout
- android:id="@+id/nav_fixed_items"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:divider="@drawable/list_divider"
- android:elevation="2dp"
- android:orientation="vertical"
- android:showDividers="beginning"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent">
-
- <TextView
- android:id="@+id/textView2"
- style="@style/nav_button"
- android:layout_weight="1"
- android:drawableStart="@drawable/ic_settings_black_24dp"
- android:text="@string/action_settings" />
-
- </LinearLayout>
-
- <ScrollView
- android:layout_width="0dp"
- android:layout_height="0dp"
- app:layout_constraintBottom_toTopOf="@+id/nav_fixed_items"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent">
-
- <LinearLayout
- android:id="@+id/nav_upper"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true"
- android:orientation="vertical"
- android:showDividers="beginning"
- app:layout_constraintBottom_toTopOf="@+id/nav_fixed_items"
- app:layout_constraintTop_toBottomOf="@+id/nav_header">
-
- <include layout="@layout/nav_header_layout" />
-
- <LinearLayout
- android:id="@+id/nav_actions"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
-
- <TextView
- android:id="@+id/nav_account_summary"
- style="@style/nav_button"
- android:drawableStart="@drawable/ic_home_black_24dp"
- android:onClick="onAccountSummaryClicked"
- android:text="@string/account_summary_title" />
-
- <TextView
- android:id="@+id/nav_latest_transactions"
- style="@style/nav_button"
- android:drawableStart="@drawable/ic_event_note_black_24dp"
- android:onClick="onLatestTransactionsClicked"
- android:text="@string/nav_transactions_title" />
-
- <TextView
- android:id="@+id/textView5"
- style="@style/nav_button"
- android:drawableStart="@drawable/ic_assignment_black_24dp"
- android:text="@string/nav_reports_title"
- android:visibility="gone" />
-
- <include
- android:id="@+id/nav_profile_list_head_layout"
- layout="@layout/nav_profile_list_head" />
-
- <LinearLayout
- android:id="@+id/nav_profile_list_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true"
- android:nestedScrollingEnabled="false"
- android:orientation="vertical">
-
- <androidx.recyclerview.widget.RecyclerView
- android:id="@+id/nav_profile_list"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:isScrollContainer="false"
- android:nestedScrollingEnabled="false"
- android:orientation="vertical">
-
- </androidx.recyclerview.widget.RecyclerView>
-
- </LinearLayout>
-
- </LinearLayout>
-
- </LinearLayout>
- </ScrollView>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
-</com.google.android.material.navigation.NavigationView>
\ No newline at end of file
android:orientation="horizontal"
android:padding="@dimen/activity_vertical_margin"
app:layout_constraintTop_toTopOf="parent"
- tools:showIn="@layout/main_navigation">
+ >
<include layout="@layout/nav_header_logo" />
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:gravity="center_vertical"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<TextView
android:layout_width="match_parent"
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="@dimen/thumb_row_height">
-
- <ImageView
- android:id="@+id/nav_new_profile_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:paddingStart="8dp"
- android:paddingEnd="8dp"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/nav_profile_list_head_buttons"
- app:layout_constraintStart_toEndOf="@id/nav_profiles_label"
- app:layout_constraintTop_toTopOf="parent"
- app:srcCompat="@drawable/ic_add_circle_white_24dp"
- android:contentDescription="@string/icon" />
-
- <LinearLayout
- android:id="@+id/nav_profile_list_head_buttons"
- android:layout_width="wrap_content"
- android:layout_height="0dp"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:paddingStart="16dp"
- android:paddingEnd="16dp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent">
-
- <ImageView
- android:id="@+id/nav_profiles_cancel_edit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/ic_clear_accent_24dp"
- android:gravity="end|center_vertical"
- android:paddingStart="8dp"
- android:paddingEnd="8dp"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- android:contentDescription="@string/icon" />
-
- <ImageView
- android:id="@+id/nav_profiles_start_edit"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/ic_settings_black_24dp"
- android:gravity="end|center_vertical"
- android:paddingStart="8dp"
- android:paddingEnd="8dp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- android:contentDescription="@string/icon" />
-
- </LinearLayout>
-
- <TextView
- android:id="@+id/nav_profiles_label"
- style="@style/nav_button"
- android:layout_width="wrap_content"
- android:layout_height="0dp"
- android:gravity="start|center_vertical"
- android:text="@string/profiles"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ >
+
+ <TextView
+ android:id="@+id/dummy_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:ignore="MissingConstraints"
+ />
+ <net.ktnx.mobileledger.ui.AutoCompleteTextViewWithClear
+ android:id="@+id/account_row_acc_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="9"
+ android:width="0dp"
+ android:hint="@string/new_transaction_account_hint"
+ android:imeOptions="actionNext"
+ android:inputType="text"
+ android:selectAllOnFocus="false"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/comment_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toStartOf="@id/amount_layout"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/account_row_acc_name"
+ >
+
+ <TextView
+ android:id="@+id/account_comment_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_comment_gray_24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <net.ktnx.mobileledger.ui.EditTextWithClear
+ android:id="@+id/comment"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="text"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/account_comment_button"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/amount_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/account_row_acc_name"
+ >
+
+ <TextView
+ android:id="@+id/currencyButton"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:minWidth="30dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="@id/currency"
+ app:layout_constraintStart_toStartOf="@id/currency"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ <EditText
+ android:id="@+id/account_row_acc_amounts"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:layout_weight="0"
+ android:width="0dp"
+ android:foregroundGravity="bottom"
+ android:gravity="bottom|end"
+ android:hint="@string/zero_amount"
+ android:imeOptions="actionNext"
+ android:inputType="number|numberSigned|numberDecimal"
+ android:minEms="4"
+ android:selectAllOnFocus="true"
+ android:textAlignment="viewEnd"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toLeftOf="@id/currency"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_min="@dimen/text_margin"
+ />
+
+ <TextView
+ android:id="@+id/currency"
+ style="@style/TextAppearance.AppCompat.Widget.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:gravity="center_horizontal"
+ android:minWidth="30dp"
+ android:text="@string/currency_symbol"
+ android:textAllCaps="false"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:orientation="horizontal"
+ >
+
+ <TextView
+ android:id="@+id/dummy_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:ignore="MissingConstraints"
+ />
+ <EditText
+ android:id="@+id/new_transaction_date"
+ android:layout_width="94dp"
+ android:layout_height="wrap_content"
+ android:accessibilityTraversalBefore="@+id/new_transaction_description"
+ android:drawableStart="@drawable/ic_event_gray_24dp"
+ android:enabled="true"
+ android:focusable="false"
+ android:gravity="bottom|center"
+ android:hint="@string/new_transaction_date_hint"
+ android:inputType="none"
+ android:nextFocusDown="@+id/new_transaction_acc_1"
+ android:nextFocusForward="@+id/new_transaction_description"
+ android:textAlignment="gravity"
+ android:textCursorDrawable="@android:color/transparent"
+ app:layout_constrainedHeight="true"
+ app:layout_constraintHorizontal_weight="8"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="TextFields"
+ />
+
+ <net.ktnx.mobileledger.ui.AutoCompleteTextViewWithClear
+ android:id="@+id/new_transaction_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:layout_marginStart="8dp"
+ android:accessibilityTraversalAfter="@+id/new_transaction_date"
+ android:foregroundGravity="bottom"
+ android:gravity="bottom"
+ android:hint="@string/new_transaction_description_hint"
+ android:imeOptions="actionNext"
+ android:inputType="text"
+ android:nextFocusLeft="@+id/new_transaction_date"
+ android:nextFocusUp="@+id/new_transaction_date"
+ android:selectAllOnFocus="false"
+ android:singleLine="true"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_weight="30"
+ app:layout_constraintStart_toEndOf="@id/new_transaction_date"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/transaction_comment_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/new_transaction_description"
+ >
+
+ <TextView
+ android:id="@+id/transaction_comment_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/ic_comment_gray_24dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <net.ktnx.mobileledger.ui.EditTextWithClear
+ android:id="@+id/transaction_comment"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="text"
+ android:visibility="invisible"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/transaction_comment_button"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true"
- android:orientation="vertical">
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/ntr_data"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true"
- android:orientation="horizontal"
- >
-
- <TextView
- android:id="@+id/dummy_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:visibility="gone"
- tools:ignore="MissingConstraints"
- />
- <EditText
- android:id="@+id/new_transaction_date"
- android:layout_width="94dp"
- android:layout_height="wrap_content"
- android:accessibilityTraversalBefore="@+id/new_transaction_description"
- android:drawableStart="@drawable/ic_event_gray_24dp"
- android:enabled="true"
- android:focusable="false"
- android:gravity="bottom|center"
- android:hint="@string/new_transaction_date_hint"
- android:inputType="none"
- android:nextFocusDown="@+id/new_transaction_acc_1"
- android:nextFocusForward="@+id/new_transaction_description"
- android:textAlignment="gravity"
- android:textCursorDrawable="@android:color/transparent"
- app:layout_constrainedHeight="true"
- app:layout_constraintHorizontal_weight="8"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- tools:ignore="TextFields" />
-
- <net.ktnx.mobileledger.ui.AutoCompleteTextViewWithClear
- android:id="@+id/new_transaction_description"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- android:layout_marginStart="8dp"
- android:accessibilityTraversalAfter="@+id/new_transaction_date"
- android:foregroundGravity="bottom"
- android:gravity="bottom"
- android:hint="@string/new_transaction_description_hint"
- android:imeOptions="actionNext"
- android:inputType="text"
- android:nextFocusLeft="@+id/new_transaction_date"
- android:nextFocusUp="@+id/new_transaction_date"
- android:selectAllOnFocus="false"
- android:singleLine="true"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintHorizontal_weight="30"
- app:layout_constraintStart_toEndOf="@id/new_transaction_date"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/transaction_comment_layout"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/new_transaction_description">
-
- <TextView
- android:id="@+id/comment_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/ic_comment_gray_24dp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <net.ktnx.mobileledger.ui.EditTextWithClear
- android:id="@+id/transaction_comment"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:imeOptions="actionNext"
- android:inputType="text"
- android:visibility="invisible"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@id/comment_button"
- app:layout_constraintTop_toTopOf="parent" />
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/ntr_account"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:animateLayoutChanges="true">
-
- <net.ktnx.mobileledger.ui.AutoCompleteTextViewWithClear
- android:id="@+id/account_row_acc_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="9"
- android:width="0dp"
- android:hint="@string/new_transaction_account_hint"
- android:imeOptions="actionNext"
- android:inputType="text"
- android:selectAllOnFocus="false"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/comment_layout"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toStartOf="@id/amount_layout"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/account_row_acc_name">
-
- <TextView
- android:id="@id/comment_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/ic_comment_gray_24dp"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <net.ktnx.mobileledger.ui.EditTextWithClear
- android:id="@+id/comment"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:imeOptions="actionNext"
- android:inputType="text"
- android:visibility="invisible"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toEndOf="@id/comment_button"
- app:layout_constraintTop_toTopOf="parent" />
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/amount_layout"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/account_row_acc_name">
-
- <EditText
- android:id="@+id/account_row_acc_amounts"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom|end"
- android:layout_weight="0"
- android:width="0dp"
- android:foregroundGravity="bottom"
- android:gravity="bottom|end"
- android:hint="@string/zero_amount"
- android:imeOptions="actionNext"
- android:inputType="number|numberSigned|numberDecimal"
- android:minEms="4"
- android:selectAllOnFocus="true"
- android:textAlignment="viewEnd"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintRight_toLeftOf="@id/currency"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_min="@dimen/text_margin" />
-
- <TextView
- android:id="@+id/currency"
- style="@style/TextAppearance.AppCompat.Widget.Button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:gravity="center_horizontal"
- android:minWidth="24dp"
- android:text="@string/currency_symbol"
- android:textAllCaps="false"
- android:visibility="gone"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintRight_toRightOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- </androidx.constraintlayout.widget.ConstraintLayout>
-
- <FrameLayout
- android:id="@+id/ntr_padding"
- android:layout_width="match_parent"
- android:layout_height="80dp"
- android:minHeight="80dp">
-
- </FrameLayout>
-
-</LinearLayout>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/no_profiles_layout"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?table_row_dark_bg"
- >
-
- <FrameLayout
- android:id="@+id/welcome_header"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent">
-
- <include layout="@layout/nav_header_layout" />
- </FrameLayout>
-
- <androidx.constraintlayout.widget.ConstraintLayout
- android:layout_width="0dp"
- android:layout_height="0dp"
- app:layout_constraintTop_toBottomOf="@id/welcome_header"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- android:padding="@dimen/activity_horizontal_margin">
-
- <TextView
- android:id="@+id/textView"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginTop="48dp"
- android:text="@string/text_welcome"
- android:textColor="?textColor"
- android:textSize="48sp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <TextView
- android:id="@+id/textView3"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:layout_marginTop="24dp"
- android:layout_marginEnd="8dp"
- android:text="@string/text_welcome_profile_needed"
- android:textColor="?textColor"
- android:textSize="20sp"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/textView" />
-
- <Button
- android:id="@+id/btn_no_profiles_add"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:layout_marginTop="24dp"
- android:layout_marginEnd="8dp"
- android:layout_marginBottom="8dp"
- android:backgroundTint="?colorSecondary"
- android:textColor="@color/design_default_color_on_primary"
- android:drawablePadding="16dp"
- android:text="@string/create_profile_label"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/textView3" />
- </androidx.constraintlayout.widget.ConstraintLayout>
-</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
- tools:context=".ui.profiles.ProfileDetailFragment">
+ tools:context=".ui.profiles.ProfileDetailFragment"
+ >
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/profile_name_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dp">
+ android:layout_marginBottom="16dp"
+ >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/profile_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/profile_name_label"
- android:inputType="textPersonName" />
+ android:inputType="textPersonName"
+ />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/url"
android:layout_height="wrap_content"
android:hint="@string/url_label"
android:inputType="textUri"
- android:text="@string/pref_default_backend_url" />
+ android:text="@string/pref_default_backend_url"
+ />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/enable_http_auth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/pref_title_use_http_auth"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<LinearLayout
android:id="@+id/auth_params"
android:animateLayoutChanges="true"
android:orientation="vertical"
android:paddingStart="8dp"
- tools:ignore="RtlSymmetry">
+ tools:ignore="RtlSymmetry"
+ >
<LinearLayout
android:id="@+id/insecure_scheme_text"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:background="?colorError"
android:padding="@dimen/activity_vertical_margin"
- android:visibility="gone">
+ android:visibility="gone"
+ >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:text="@string/insecure_scheme_with_auth"
android:textColor="?colorOnError"
- android:text="@string/insecure_scheme_with_auth" />
+ />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/auth_user_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/pref_title_backend_auth_user"
- android:inputType="textPersonName" />
+ android:inputType="textPersonName"
+ />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
- app:passwordToggleEnabled="true">
+ app:passwordToggleEnabled="true"
+ >
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/pref_title_backend_auth_password"
- android:inputType="textWebPassword" />
+ android:inputType="textWebPassword"
+ />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/api_version_layout"
+ android:id="@+id/server_version_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dp">
+ android:layout_marginBottom="16dp"
+ >
<TextView
- android:id="@+id/api_version_label"
- android:layout_width="match_parent"
+ android:id="@+id/server_version_label"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:text="@string/profile_api_version_title"
+ android:text="@string/profile_server_version_title"
android:textAppearance="?android:textAppearanceListItem"
+ app:layout_constraintEnd_toStartOf="@id/server_version_detect_button"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<TextView
- android:id="@+id/api_version_text"
+ android:id="@+id/detected_server_version_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:gravity="start"
android:textAppearance="?android:textAppearanceListItemSecondary"
android:textColor="?attr/textColor"
- app:layout_constraintEnd_toStartOf="@id/detected_version_text"
+ android:text="@string/server_version_unknown_label"
+ app:layout_constraintEnd_toStartOf="@id/server_version_detect_button"
+ app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/api_version_label"
+ app:layout_constraintTop_toBottomOf="@id/server_version_label"
+ />
+ <ProgressBar
+ android:layout_height="24dp"
+ android:id="@+id/server_version_detect_button"
+ android:layout_width="24dp"
+ android:indeterminate="true"
+ android:foregroundGravity="bottom"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/detected_server_version_text"
+ android:visibility="invisible"
/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:id="@+id/api_version_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ >
+
<TextView
- android:id="@+id/detected_version_label"
+ android:id="@+id/api_version_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:gravity="end"
- android:text="@string/detected_version_label"
- android:textAppearance="?android:textAppearanceListItemSecondary"
- app:layout_constraintEnd_toStartOf="@id/detected_version_text"
+ android:layout_marginEnd="24dp"
+ android:text="@string/profile_api_version_title"
+ android:textAppearance="?android:textAppearanceListItem"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/api_version_text"
+ app:layout_constraintTop_toTopOf="parent"
/>
+
<TextView
- android:id="@+id/detected_version_text"
- android:layout_width="wrap_content"
+ android:id="@+id/api_version_text"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_marginEnd="8dp"
- android:text="@string/api_version_unknown_label"
+ android:layout_marginEnd="24dp"
android:textAppearance="?android:textAppearanceListItemSecondary"
android:textColor="?attr/textColor"
- app:layout_constraintEnd_toStartOf="@id/api_version_detect_button"
- app:layout_constraintTop_toBottomOf="@id/api_version_text"
- />
- <TextView
- android:id="@+id/api_version_detect_button"
- android:layout_width="24dp"
- android:layout_height="0dp"
- android:drawableBottom="@drawable/ic_refresh_primary_24dp"
- android:foregroundGravity="bottom"
- app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/api_version_label"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/profile_permit_posting"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/posting_permitted"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<LinearLayout
android:id="@+id/posting_sub_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<LinearLayout
android:id="@+id/default_commodity_layout"
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_default_commodity"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<TextView
android:id="@+id/default_commodity_text"
android:layout_height="wrap_content"
android:text="@string/btn_no_currency"
android:textAppearance="?android:textAppearanceListItemSecondary"
- android:textColor="?attr/textColor" />
+ android:textColor="?attr/textColor"
+ />
</LinearLayout>
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/profile_show_commodity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/currency_input_by_default"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/profile_show_comments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/show_comment_input_by_default"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<LinearLayout
android:id="@+id/future_dates_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<TextView
android:id="@+id/future_dates_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_future_dates_label"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<TextView
android:id="@+id/future_dates_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceListItemSecondary"
- android:textColor="?attr/textColor" />
+ android:textColor="?attr/textColor"
+ />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
- android:id="@+id/preferred_accounts_accounts_filter_layout"
+ android:id="@+id/preferred_accounts_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ >
<com.google.android.material.textfield.TextInputEditText
- android:id="@+id/preferred_accounts_filter_filter"
+ android:id="@+id/preferred_accounts_filter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:hint="@string/pref_preferred_autocompletion_account_filter_hint"
android:inputType="text"
- android:textColor="?attr/editTextColor" />
+ android:textColor="?attr/editTextColor"
+ />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="horizontal">
+ android:orientation="horizontal"
+ >
<TextView
android:layout_width="wrap_content"
android:layout_weight="100"
android:gravity="center_vertical"
android:text="@string/profile_color_label"
- android:textAppearance="?android:textAppearanceListItem" />
+ android:textAppearance="?android:textAppearanceListItem"
+ />
<ImageButton
android:id="@+id/btn_pick_ring_color"
android:layout_weight="1"
android:background="?colorPrimary"
android:contentDescription="@string/btn_color_picker_button"
- app:srcCompat="@drawable/ic_palette_black_24dp"
android:tint="?colorOnPrimarySurface"
+ app:srcCompat="@drawable/ic_palette_black_24dp"
/>
</LinearLayout>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2020 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:tools="http://schemas.android.com/tools"
- android:name="net.ktnx.mobileledger.ui.activity.ProfileListFragment"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- app:layoutManager="LinearLayoutManager"
- tools:listitem="@layout/profile_list_content" />
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="9"
- android:background="@drawable/ic_unfold_more_black_24dp"
+ android:background="@drawable/ic_baseline_drag_handle_24"
android:contentDescription="@string/profile_list_rearrange_handle_label"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
<!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2020 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
android:layout_width="match_parent"
android:layout_height="match_parent">
- <Switch
+ <com.google.android.material.switchmaterial.SwitchMaterial
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
- android:layout_centerVertical="true" />
+ android:layout_centerVertical="true"
+ />
</RelativeLayout>
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/pattern_details_item_account_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:padding="@dimen/text_margin"
+ >
+ <TextView
+ android:id="@+id/pattern_account_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:clickable="false"
+ android:text="@string/template_details_account_row_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:drawableBottomCompat="@drawable/dashed_border_8dp"
+ app:drawableStartCompat="@drawable/ic_baseline_drag_handle_24"
+ app:layout_constraintBottom_toTopOf="@id/template_account_name_source_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ <TextView
+ android:id="@+id/template_account_name_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/text_margin"
+ android:text="@string/account_name_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_name_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_account_label"
+ />
+ <TextView
+ android:id="@+id/template_details_account_name_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_name_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_account_name_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/template_details_account_name_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@+id/template_account_comment_source_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_name_source"
+ >
+ <com.google.android.material.textfield.MaterialAutoCompleteTextView
+ android:id="@+id/template_details_account_name"
+ style="@style/MoLeMaterialAutoCompleteTextViewStyle"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_details_account_name_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/template_account_comment_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:text="@string/account_comment_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_comment_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_name_layout"
+ />
+ <TextView
+ android:id="@+id/template_details_account_comment_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_comment_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_account_comment_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/template_details_account_comment_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@+id/template_account_amount_source_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_comment_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_details_account_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_details_account_comment_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/template_account_amount_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:text="@string/account_amount_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_amount_source"
+ app:layout_constraintEnd_toStartOf="@id/negate_amount_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_comment_layout"
+ />
+ <TextView
+ android:id="@+id/template_details_account_amount_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_amount_layout"
+ app:layout_constraintEnd_toStartOf="@id/negate_amount_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_account_amount_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/template_details_account_amount_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@id/template_details_negate_amount_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_amount_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_details_account_amount"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_details_account_amount_label"
+ android:inputType="number|numberDecimal|numberSigned"
+ android:selectAllOnFocus="true"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <TextView
+ android:id="@+id/template_details_negate_amount_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/template_account_negate_amount_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@id/template_details_negate_amount_text"
+ app:layout_constraintEnd_toStartOf="@+id/negate_amount_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_amount_layout"
+ />
+ <TextView
+ android:id="@+id/template_details_negate_amount_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/template_account_keep_amount_sign"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/template_details_negate_amount_text"
+ app:layout_constraintEnd_toStartOf="@+id/negate_amount_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_negate_amount_label"
+ />
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/negate_amount_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@id/template_details_negate_amount_text"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/template_details_negate_amount_label"
+ />
+
+ <TextView
+ android:id="@+id/template_account_currency_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/account_currency_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_currency_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/negate_amount_switch"
+ />
+ <TextView
+ android:id="@+id/template_details_account_currency_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/template_details_account_currency"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_account_currency_source_label"
+ />
+ <TextView
+ android:id="@+id/template_details_account_currency"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginVertical="@dimen/half_text_margin"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_details_account_currency_source"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/pattern_details_recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ui.templates.TemplateDetailsFragment"
+ >
+</androidx.recyclerview.widget.RecyclerView>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/pattern_details_item_head"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:animateLayoutChanges="true"
+ android:padding="@dimen/text_margin"
+ >
+ <ImageButton
+ android:id="@+id/template_params_help_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/template_params_help_description"
+ android:minWidth="@dimen/thumb_row_height"
+ android:src="@drawable/ic_baseline_help_outline_24_primary"
+ app:layout_constraintBottom_toBottomOf="@id/template_params_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/template_params_label"
+ />
+ <TextView
+ android:id="@+id/template_params_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:gravity="end"
+ android:text="@string/template_details_template_params_label"
+ android:textAppearance="@android:style/TextAppearance.Material.Medium"
+ app:layout_constraintBottom_toTopOf="@id/pattern_name_layout"
+ app:layout_constraintEnd_toStartOf="@id/template_params_help_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_name_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@id/pattern_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_params_label"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_name_label"
+ android:inputType="text"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/pattern_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@id/pattern_hint_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_name_layout"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/pattern"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_details_pattern_label"
+ android:inputType="text|textMultiLine"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <TextView
+ android:id="@+id/pattern_hint_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/pattern_match_result"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
+ app:layout_constraintBottom_toTopOf="@+id/pattern_hint_text"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_layout"
+ />
+ <TextView
+ android:id="@+id/pattern_hint_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
+ app:layout_constraintBottom_toTopOf="@+id/test_text_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_hint_title"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/test_text_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@id/transaction_parameters_label"
+ app:layout_constraintEnd_toStartOf="@id/template_details_head_scan_qr_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/pattern_hint_text"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/test_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_details_test_text_label"
+ android:inputType="text|textMultiLine"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <ImageButton
+ android:id="@+id/template_details_head_scan_qr_button"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:background="@android:color/transparent"
+ android:contentDescription="@string/scan_qr"
+ android:minWidth="@dimen/thumb_row_height"
+ app:layout_constraintBottom_toBottomOf="@id/test_text_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/test_text_layout"
+ app:srcCompat="@drawable/ic_baseline_qr_code_scanner_24"
+ app:tint="?colorPrimary"
+ />
+ <TextView
+ android:id="@+id/transaction_parameters_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ android:gravity="end"
+ android:text="@string/template_transaction_parameters_label"
+ android:textAppearance="@android:style/TextAppearance.Material.Medium"
+ app:layout_constraintBottom_toTopOf="@+id/transaction_date_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/test_text_layout"
+ />
+ <TextView
+ android:id="@+id/transaction_date_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/template_details_date_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@id/year_source_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_parameters_label"
+ />
+ <TextView
+ android:id="@+id/year_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/template_details_date_year_source_label"
+ android:textAlignment="center"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ app:layout_constraintBottom_toTopOf="@+id/year_source"
+ app:layout_constraintEnd_toStartOf="@id/month_source_label"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/month_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/month_source_label"
+ android:textAlignment="center"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ app:layout_constraintBottom_toTopOf="@+id/month_source"
+ app:layout_constraintEnd_toStartOf="@id/day_source_label"
+ app:layout_constraintStart_toEndOf="@id/year_source_label"
+ app:layout_constraintTop_toBottomOf="@id/transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/day_source_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/template_details_date_day_source_label"
+ android:textAlignment="center"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1"
+ app:layout_constraintBottom_toTopOf="@+id/day_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/month_source_label"
+ app:layout_constraintTop_toBottomOf="@id/transaction_date_label"
+ />
+ <TextView
+ android:id="@+id/year_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/template_details_source_literal"
+ android:textAlignment="center"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ android:layout_marginHorizontal="@dimen/half_text_margin"
+ app:layout_constraintBottom_toTopOf="@+id/year_layout"
+ app:layout_constraintEnd_toStartOf="@id/month_source"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/day_source_label"
+ />
+ <TextView
+ android:id="@+id/month_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/half_text_margin"
+ android:text="@string/template_details_source_literal"
+ android:textAlignment="center"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/month_layout"
+ app:layout_constraintEnd_toStartOf="@id/day_source"
+ app:layout_constraintStart_toEndOf="@id/year_source"
+ app:layout_constraintTop_toBottomOf="@id/month_source_label"
+ />
+ <TextView
+ android:id="@+id/day_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/half_text_margin"
+ android:text="@string/template_details_source_literal"
+ android:textAlignment="center"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ app:layout_constraintBottom_toTopOf="@+id/day_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/month_source"
+ app:layout_constraintTop_toBottomOf="@id/day_source_label"
+ />
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier_before_date_inputs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="year_source,month_source,day_source"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/year_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="@id/year_source_label"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_details_date_year"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_year_hint"
+ android:inputType="number"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/month_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="@id/month_source_label"
+ app:layout_constraintStart_toStartOf="@id/month_source_label"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_details_date_month"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_month_hint"
+ android:inputType="number"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/day_layout"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="@id/day_source_label"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_date_inputs"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/template_details_date_day"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:hint="@string/date_day_hint"
+ android:inputType="number"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier_before_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="day_layout,month_layout,year_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+ <TextView
+ android:id="@+id/template_transaction_description_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/text_margin"
+ android:text="@string/transaction_description_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_transaction_description_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/barrier_before_description"
+ />
+ <TextView
+ android:id="@+id/template_transaction_description_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ android:text="@string/template_details_source_literal"
+ app:layout_constraintBottom_toTopOf="@+id/transaction_description_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_transaction_description_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/transaction_description_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@+id/template_transaction_comment_source_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_transaction_description_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/transaction_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_transaction_description_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <TextView
+ android:id="@+id/template_transaction_comment_source_label"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:text="@string/transaction_comment_source_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_transaction_comment_source"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_description_layout"
+ />
+ <TextView
+ android:id="@+id/template_transaction_comment_source"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minWidth="100dp"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ android:text="@string/template_details_source_literal"
+ app:layout_constraintBottom_toTopOf="@+id/transaction_comment_layout"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_transaction_comment_source_label"
+ />
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/transaction_comment_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/text_margin"
+ app:endIconMode="clear_text"
+ app:layout_constraintBottom_toTopOf="@id/template_is_fallback_label"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_transaction_comment_source"
+ >
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/transaction_comment"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/template_transaction_comment_hint"
+ />
+ </com.google.android.material.textfield.TextInputLayout>
+ <TextView
+ android:id="@+id/template_is_fallback_label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:text="@string/template_is_fallback_label"
+ android:textAppearance="?attr/textAppearanceListItem"
+ app:layout_constraintBottom_toTopOf="@+id/template_is_fallback_text"
+ app:layout_constraintEnd_toStartOf="@id/template_is_fallback_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/transaction_comment_layout"
+ />
+ <TextView
+ android:id="@+id/template_is_fallback_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?attr/textAppearanceListItemSecondary"
+ android:text="@string/template_is_fallback_no"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/template_is_fallback_switch"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/template_is_fallback_label"
+ />
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/template_is_fallback_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="@id/template_is_fallback_text"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="@id/template_is_fallback_label"
+ />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ <TextView
+ android:id="@+id/template_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/text_margin"
+ android:gravity="center_vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ android:minHeight="@dimen/thumb_row_height"
+ android:textAppearance="@android:style/TextAppearance.Material.Medium"
+ app:layout_constraintEnd_toEndOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ >
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="?table_row_light_bg"
+ android:gravity="center_vertical|end"
+ android:minHeight="@dimen/thumb_row_height"
+ android:paddingHorizontal="@dimen/text_margin"
+ android:paddingVertical="@dimen/half_text_margin"
+ android:text="@string/fallback_templates_divider"
+ android:textAppearance="@android:style/TextAppearance.Material.Widget.Toolbar.Title"
+ android:textColor="?android:textColorSecondary"
+ android:textStyle="italic"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/transaction_delimiter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="16dp"
+ android:layout_marginEnd="8dp"
+ >
+
+ <TextView
+ android:id="@+id/transaction_delimiter_month"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="4dp"
+ android:text="---------"
+ android:textColor="?colorPrimary"
+ android:textStyle="bold"
+ app:layout_constraintEnd_toEndOf="parent"
+ tools:ignore="HardcodedText"
+ />
+
+ <TextView
+ android:id="@+id/transaction_delimiter_date"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginHorizontal="4dp"
+ android:text="--.--.----"
+ android:textColor="?colorPrimary"
+ android:textStyle="bold"
+ app:layout_constraintStart_toStartOf="parent"
+ tools:ignore="HardcodedText"
+ />
+
+ <View
+ android:id="@+id/transaction_delimiter_thick"
+ android:layout_width="0dp"
+ android:layout_height="1dp"
+ android:layout_marginHorizontal="16dp"
+ android:background="?colorPrimary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/transaction_delimiter_month"
+ app:layout_constraintStart_toEndOf="@id/transaction_delimiter_date"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:measureAllChildren="false"
>
<com.google.android.material.card.MaterialCardView
<LinearLayout
android:id="@+id/transaction_row_head"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
<LinearLayout
android:id="@+id/transaction_row_acc_amounts"
- android:layout_width="match_parent"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
- app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/transaction_running_total"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/transaction_row_head"
>
<include layout="@layout/transaction_list_row_accounts_table_row" />
</LinearLayout>
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:barrierDirection="top"
+ app:constraint_referenced_ids="transaction_row_acc_amounts,transaction_running_total"
+ />
+
+ <TextView
+ android:id="@+id/transaction_running_total"
+ style="@style/transaction_list_comment"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/half_text_margin"
+ android:gravity="bottom|end"
+ android:text="one two"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="@id/transaction_row_acc_amounts"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/transaction_row_acc_amounts"
+ app:layout_goneMarginStart="0dp"
+ />
+ <View
+ android:id="@+id/transaction_running_total_divider"
+ android:layout_width="1dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="@dimen/quarter_text_margin"
+ android:background="?commentColor"
+ app:layout_constraintBottom_toBottomOf="@id/transaction_running_total"
+ app:layout_constraintStart_toEndOf="@id/transaction_row_acc_amounts"
+ app:layout_constraintTop_toBottomOf="@id/barrier"
+ app:layout_goneMarginStart="0dp"
+ />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
- <androidx.constraintlayout.widget.ConstraintLayout
- android:id="@+id/transaction_delimiter"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
- android:layout_marginTop="16dp"
- android:layout_marginEnd="8dp"
- >
-
- <TextView
- android:id="@+id/transaction_delimiter_month"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginHorizontal="4dp"
- android:text="---------"
- android:textColor="?colorPrimary"
- android:textStyle="bold"
- app:layout_constraintEnd_toEndOf="parent"
- tools:ignore="HardcodedText"
- />
-
- <TextView
- android:id="@+id/transaction_delimiter_date"
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:layout_marginHorizontal="4dp"
- android:text="--.--.----"
- android:textColor="?colorPrimary"
- android:textStyle="bold"
- app:layout_constraintStart_toStartOf="parent"
- tools:ignore="HardcodedText"
- />
-
- <View
- android:id="@+id/transaction_delimiter_thick"
- android:layout_width="0dp"
- android:layout_height="1dp"
- android:layout_marginHorizontal="16dp"
- android:background="?colorPrimary"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintEnd_toStartOf="@id/transaction_delimiter_month"
- app:layout_constraintStart_toEndOf="@id/transaction_delimiter_date"
- app:layout_constraintTop_toTopOf="parent"
- />
-
- </androidx.constraintlayout.widget.ConstraintLayout>
- <include layout="@layout/last_update_layout" />
</FrameLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
android:id="@+id/transaction_list_acc_row_acc_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:text="another acc name"
+ android:breakStrategy="high_quality"
android:textAlignment="viewStart"
android:textAppearance="@android:style/TextAppearance.Material.Small"
tools:ignore="HardcodedText"
+ android:hyphenationFrequency="full"
+ android:text="one:very:long:account:name:that:needs:to:wrap:to:more:tnan:one:line:two:would:be:nice:but:the:more:the:better:and:the:better:"
/>
<TextView
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_marginEnd="0dp"
+ android:layout_marginStart="@dimen/half_text_margin"
android:minWidth="60dp"
android:text="---,--"
android:textAlignment="viewEnd"
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2024 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <item
+ android:id="@+id/menu_account_list_show_zero_balances"
+ android:checkable="true"
+ android:enabled="true"
+ android:menuCategory="container"
+ android:title="@string/accounts_menu_show_zero"
+ android:titleCondensed="@string/accounts_menu_show_zero_condensed"
+ android:visible="true"
+ app:showAsAction="withText"
+ />
+</menu>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<menu xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- tools:context="net.ktnx.mobileledger.ui.activity.MainActivity">
-
- <item
- android:id="@+id/menu_acc_summary_only_starred"
- android:checkable="true"
- android:checked="false"
- android:title="@string/menu_acc_summary_show_only_starred_title"
- app:actionLayout="@layout/switch_item"
- app:showAsAction="never" />
- <item android:id="@+id/menu_acc_summary_hide_selected"
- android:icon="@drawable/ic_star_white_24dp"
- android:title="@string/menu_acc_summary_hide_selected_title"
- app:showAsAction="always"
- android:visible="false"
- />
- <item android:id="@+id/menu_acc_summary_cancel_selection"
- android:title="@string/menu_acc_summary_cancel_selection_title"
- app:showAsAction="always"
- android:visible="false"
- android:icon="@drawable/ic_cancel_white_24dp" />
- <item android:id="@+id/menu_acc_summary_confirm_selection"
- android:title="@string/menu_acc_summary_confirm_selection_title"
- app:showAsAction="always"
- android:visible="false"
- android:icon="@drawable/ic_check_white_24dp" />
-</menu>
\ No newline at end of file
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2020 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<item
android:id="@+id/api_version_menu_auto"
- android:title="@string/api_auto" />
+ android:title="@string/api_auto"
+ />
<item
- android:id="@+id/api_version_menu_post_1_14"
- android:title="@string/api_post_1_14" />
+ android:id="@+id/api_version_menu_1_23"
+ android:title="@string/api_1_23"
+ />
<item
- android:id="@+id/api_version_menu_pre_1_15"
- android:title="@string/api_pre_1_15" />
+ android:id="@+id/api_version_menu_1_19_1"
+ android:title="@string/api_1_19_1"
+ />
+ <item
+ android:id="@+id/api_version_menu_1_15"
+ android:title="@string/api_1_15"
+ />
+ <item
+ android:id="@+id/api_version_menu_1_14"
+ android:title="@string/api_1_14"
+ />
<item
android:id="@+id/api_version_menu_html"
- android:title="@string/api_html" />
+ android:title="@string/api_html"
+ />
</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <item
- android:id="@+id/action_simulate_crash"
- android:title="@string/crash_app_label"
- android:titleCondensed="@string/crash_app_condensed_label"
- android:onClick="simulateCrash"
- android:visible="false"
- app:showAsAction="never" />
- <item
- android:id="@+id/action_simulate_save"
- android:checkable="true"
- android:checked="false"
- android:onClick="toggleSimulateSave"
- android:title="@string/simulate_save_label"
- android:titleCondensed="@string/simulate_save_condensed_label"
- android:visible="false"
- app:showAsAction="never" />
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+ <group android:id="@+id/new_transaction_debug_menu_items">
+ <item
+ android:id="@+id/action_simulate_crash"
+ android:title="@string/crash_app_label"
+ android:titleCondensed="@string/crash_app_condensed_label"
+ app:showAsAction="never"
+ />
+ <item
+ android:id="@+id/action_simulate_save"
+ android:checkable="true"
+ android:checked="false"
+ android:title="@string/simulate_save_label"
+ android:titleCondensed="@string/simulate_save_condensed_label"
+ app:showAsAction="never"
+ />
+ </group>
</menu>
\ No newline at end of file
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
- <item
- android:id="@+id/toggle_currency"
- android:title="@string/show_currency_input"
- android:checkable="true"
- android:checked="false"
- app:showAsAction="never" />
- <item
- android:id="@+id/toggle_comments"
- android:checkable="true"
- android:title="@string/show_comments_switch"
- app:actionLayout="@layout/switch_item"
- app:showAsAction="never" />
- <item
- android:id="@+id/action_reset_new_transaction_activity"
- android:icon="@drawable/ic_refresh_white_24dp"
- android:title="@string/action_reset_new_transaction_activity_title"
- app:showAsAction="never" />
-
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+ <group android:id="@+id/new_transaction_fragment_menu_items">
+ <item
+ android:id="@+id/scan_qr"
+ android:icon="@drawable/ic_baseline_qr_code_scanner_24"
+ android:title="@string/scan_qr"
+ app:showAsAction="ifRoom"
+ />
+ <item
+ android:id="@+id/toggle_currency"
+ android:checkable="true"
+ android:checked="false"
+ android:title="@string/show_currency_input"
+ app:actionLayout="@layout/switch_item"
+ app:showAsAction="never"
+ />
+ <item
+ android:id="@+id/toggle_comments"
+ android:checkable="true"
+ android:title="@string/show_comments_switch"
+ app:actionLayout="@layout/switch_item"
+ app:showAsAction="never"
+ />
+ <item
+ android:id="@+id/action_reset_new_transaction_activity"
+ android:icon="@drawable/ic_refresh_white_24dp"
+ android:title="@string/action_reset_new_transaction_activity_title"
+ app:showAsAction="never"
+ />
+ </group>
</menu>
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <item
- android:id="@+id/menu_add_profile"
- android:icon="@drawable/ic_add_circle_white_24dp"
- android:title="@string/create_profile_label"
- app:showAsAction="ifRoom" />
-</menu>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <item
+ android:id="@+id/delete_template"
+ android:icon="@drawable/ic_delete_white_24dp"
+ android:title="@string/Remove"
+ app:showAsAction="always"
+ />
+</menu>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ >
+
+ <item
+ android:icon="@drawable/ic_baseline_help_24_white"
+ android:title="@string/help_menu_item_title"
+ android:id="@+id/menu_item_template_list_help"
+ app:showAsAction="ifRoom"
+ />
+</menu>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<fragment
android:id="@+id/newTransactionFragment"
- android:name="net.ktnx.mobileledger.ui.activity.NewTransactionFragment"
+ android:name="net.ktnx.mobileledger.ui.new_transaction.NewTransactionFragment"
android:label="NewTransactionFragment"
>
<action
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright © 2021 Damyan Ivanov.
+ ~ This file is part of MoLe.
+ ~ MoLe is free software: you can distribute it and/or modify it
+ ~ under the term of the GNU General Public License as published by
+ ~ the Free Software Foundation, either version 3 of the License, or
+ ~ (at your opinion), any later version.
+ ~
+ ~ MoLe is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ ~ GNU General Public License terms for details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+ -->
+
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/template_list_navigation"
+ app:startDestination="@id/templateListFragment"
+ >
+
+ <fragment
+ android:id="@+id/templateListFragment"
+ android:name="net.ktnx.mobileledger.ui.templates.TemplateListFragment"
+ android:label="TemplateListFragment"
+ android:tag="templateListFragment"
+ >
+ <action
+ android:id="@+id/action_templateListFragment_to_templateDetailsFragment"
+ app:destination="@id/templateDetailsFragment"
+ app:enterAnim="@anim/slide_in_left"
+ app:exitAnim="@anim/slide_out_left"
+ />
+ </fragment>
+ <fragment
+ android:id="@+id/templateDetailsFragment"
+ android:name="net.ktnx.mobileledger.ui.templates.TemplateDetailsFragment"
+ android:label="pattern_details_fragment"
+ android:tag="patternDetailsFragment"
+ tools:layout="@layout/template_details_fragment"
+ >
+ <action
+ android:id="@+id/action_templateDetailsFragment_to_templateListFragment"
+ app:destination="@id/templateListFragment"
+ />
+ </fragment>
+</navigation>
\ No newline at end of file
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar, order_no integer, permit_posting boolean default 0, theme integer default -1, preferred_accounts_filter varchar, future_dates integer, api_version integer, show_commodity_by_default boolean default 0, default_commodity varchar, show_comments_by_default boolean default 1, detected_version_pre_1_19 boolean, detected_version_major integer, detected_version_minor integer);
-create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, level integer not null, parent_name varchar, expanded default 1, amounts_expanded boolean default 0, generation integer default 0);
-create unique index un_accounts on accounts(profile, name);
-create table options(profile varchar not null, name varchar not null, value varchar);
-create unique index un_options on options(profile,name);
-create table account_values(profile varchar not null, account varchar not null, currency varchar not null default '', value decimal not null, generation integer default 0 );
-create unique index un_account_values on account_values(profile,account,currency);
-create table description_history(description varchar not null primary key, description_upper varchar, generation integer default 0);
-create unique index un_description_history on description_history(description_upper);
-create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, generation integer default 0);
-create unique index un_transactions_id on transactions(profile,id);
-create unique index un_transactions_data_hash on transactions(profile,data_hash);
-create index idx_transaction_description on transactions(description);
-create table transaction_accounts(profile varchar not null, transaction_id integer not null, order_no integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, generation integer default 0, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
-create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
-create table currencies(id integer not null primary key, name varchar not null, position varchar not null, has_gap boolean not null);
--- updated to revision 39
\ No newline at end of file
--- /dev/null
+-- Copyright © 2019 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+BEGIN TRANSACTION;
+
+alter table profiles add permit_posting boolean default 0;
+update profiles set permit_posting = 1;
+
+COMMIT TRANSACTION;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2019 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+BEGIN TRANSACTION;
+
+alter table profiles add theme integer default -1;
+update profiles set theme = -1;
+
+COMMIT TRANSACTION;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2019 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+BEGIN TRANSACTION;
+
+alter table accounts add expanded default 1;
+update accounts set expanded = 1;
+
+COMMIT TRANSACTION;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2019 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+BEGIN TRANSACTION;
+
+delete from accounts where not exists (select 1 from profiles where uuid = profile);
+delete from account_values where not exists (select 1 from profiles where uuid = profile);
+delete from transactions where not exists (select 1 from profiles where uuid = profile);
+delete from transaction_accounts where not exists (select 1 from profiles where uuid = profile);
+
+COMMIT TRANSACTION;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 20 to revision 22
+
+alter table accounts add amounts_expanded boolean default 0;
+alter table profiles add preferred_accounts_filter varchar;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 22 to revision 30
+
+-- 23, 24, 27, 28
+alter table profiles
+add future_dates integer,
+add api_version integer,
+add show_commodity_by_default boolean default 0,
+add default_commodity varchar;
+
+-- 25
+create table currencies(id integer not null primary key, name varchar not null, position varchar not null, has_gap boolean not null);
+
+-- 26
+alter table transaction_accounts add comment varchar;
+
+-- 29
+create index idx_transaction_description on transactions(description);
+
+-- 30
+delete from options
+where profile <> '-'
+ and not exists (select 1 from profiles p where p.uuid=options.profile);
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 30 to revision 32
+
+-- 31
+alter table profiles add show_comments_by_default boolean default 0;
+
+-- 32
+update profiles set show_comments_by_default = 1;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 32 to revision 34
+
+-- 33 (merged below)
+-- alter table transactions add comment varchar;
+
+-- 34
+alter table transactions add year integer not null default 0;
+alter table transactions add month integer not null default 0;
+alter table transactions add day integer not null default 0;
+alter table transactions add tmp_md varchar;
+update transactions set year= cast(substr(date, 1,instr(date, '/')-1) as integer);
+update transactions set tmp_md= substr(date, instr(date, '/')+1);
+update transactions set month=cast(substr(tmp_md,1,instr(tmp_md,'/')-1) as integer);
+update transactions set day= cast(substr(tmp_md, instr(tmp_md,'/')+1) as integer);
+-- alter table transactions drop date
+create table transactions_2(
+ profile varchar not null,
+ id integer not null,
+ data_hash varchar not null,
+ year integer not null,
+ month integer not null,
+ day integer not null,
+ description varchar not null,
+ comment varchar,
+ keep boolean not null default 0);
+insert into transactions_2(profile, id, data_hash, year, month, day, description, comment, keep)
+select profile, id, data_hash, year, month, day, description, null, keep from transactions;
+
+drop table transactions;
+
+alter table transactions_2 rename to transactions;
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 34 to revision 40
+
+-- 35
+create table accounts_new(
+ profile varchar not null,
+ name varchar not null,
+ name_upper varchar not null,
+ keep boolean not null default 0,
+ level integer not null,
+ parent_name varchar,
+ expanded default 1,
+ amounts_expanded boolean default 0,
+ generation integer default 0);
+
+insert into accounts_new(
+ profile, name, name_upper, keep, level, parent_name, expanded, amounts_expanded)
+select profile, name, name_upper, keep, level, parent_name, expanded, amounts_expanded
+from accounts;
+
+drop table accounts;
+
+alter table accounts_new rename to accounts;
+
+-- 36
+-- merged in 35 --alter table accounts add generation integer default 0;
+
+alter table account_values add generation integer default 0;
+
+alter table transactions add generation integer default 0;
+
+alter table transaction_accounts
+add generation integer default 0,
+add order_no integer not null default 0;
+
+-- 37
+update transaction_accounts set order_no = rowid;
+
+-- 40
+delete from transaction_accounts where not exists (select 1 from accounts a where a.profile=transaction_accounts.profile and a.name=transaction_accounts.account_name);
+delete from transaction_accounts where not exists (select 1 from transactions t where t.profile=transaction_accounts.profile and t.id=transaction_accounts.transaction_id);
+
+-- 38
+CREATE TABLE transaction_accounts_new(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, generation integer default 0, order_no integer not null default 0, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
+insert into transaction_accounts_new(profile, transaction_id, account_name, currency, amount, comment, generation, order_no) select profile, transaction_id, account_name, currency, amount, comment, generation, order_no from transaction_accounts;
+drop table transaction_accounts;
+alter table transaction_accounts_new rename to transaction_accounts;
+create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
+
+-- 39
+create table description_history_new(description varchar not null primary key, description_upper varchar, generation integer default 0);
+insert into description_history_new(description, description_upper) select description, description_upper from description_history;
+drop table description_history;
+alter table description_history_new rename to description_history;
+create unique index un_description_history on description_history(description_upper);
+
--- /dev/null
+-- Copyright © 2020 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+BEGIN TRANSACTION;
+
+alter table profiles add detected_version_pre_1_19 boolean;
+alter table profiles add detected_version_major integer;
+alter table profiles add detected_version_minor integer;
+
+COMMIT TRANSACTION;
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 41 to revision 58
+
+-- profiles
+create table profiles_new(
+ uuid text not null,
+ name text not null,
+ url text not null,
+ use_authentication integer not null,
+ auth_user text,
+ auth_password text,
+ order_no integer not null,
+ permit_posting integer not null default 0,
+ theme integer not null default -1,
+ preferred_accounts_filter varchar,
+ future_dates integer not null,
+ api_version integer not null,
+ show_commodity_by_default integer not null default 0,
+ default_commodity text,
+ show_comments_by_default integer not null default 1,
+ detected_version_pre_1_19 integer not null,
+ detected_version_major integer not null,
+ detected_version_minor integer not null,
+ primary key(uuid));
+
+insert into profiles_new(
+ uuid, name, url, use_authentication, auth_user, auth_password, order_no,
+ permit_posting, theme, preferred_accounts_filter, future_dates, api_version,
+ show_commodity_by_default, default_commodity, show_comments_by_default,
+ detected_version_pre_1_19, detected_version_major, detected_version_minor)
+select uuid, name, url, use_authentication, auth_user, auth_password, order_no,
+ permit_posting, theme, preferred_accounts_filter, coalesce(future_dates,-1),
+ coalesce(api_version,0),
+ show_commodity_by_default, default_commodity, show_comments_by_default,
+ coalesce(detected_version_pre_1_19,0), coalesce(detected_version_major,0),
+ coalesce(detected_version_minor,0)
+from profiles;
+
+-- options
+create table options_new(profile varchar not null, name varchar not null, value varchar, primary key(profile, name));
+
+insert into options_new(profile, name, value)
+select profile, name, value from options;
+
+-- accounts
+create table accounts_new(
+ profile varchar not null,
+ name varchar not null,
+ name_upper varchar not null,
+ level integer not null,
+ parent_name varchar,
+ expanded integer not null default 1,
+ amounts_expanded integer not null default 0,
+ generation integer not null default 0,
+ primary key(profile, name));
+
+insert into accounts_new(profile, name, name_upper, level, parent_name,
+ expanded, amounts_expanded, generation)
+select profile, name, name_upper, level, parent_name, expanded,
+ amounts_expanded, generation from accounts;
+
+-- account_values
+create table account_values_new(
+ profile varchar not null,
+ account varchar not null,
+ currency varchar not null default '',
+ value real not null,
+ generation integer not null default 0,
+ primary key(profile, account, currency));
+
+insert into account_values_new(
+ profile, account, currency, value, generation)
+select profile, account, currency, value, generation
+from account_values;
+
+-- description_history
+create table description_history_new(
+ description varchar collate NOCASE not null,
+ description_upper varchar not null,
+ generation integer not null default 0,
+ primary key(description));
+
+insert into description_history_new(description, description_upper, generation)
+select description, description_upper, generation from description_history;
+
+-- transactions
+create table transactions_new(
+ profile varchar not null,
+ id integer not null,
+ data_hash varchar not null,
+ year integer not null,
+ month integer not null,
+ day integer not null,
+ description varchar collate NOCASE not null,
+ comment varchar,
+ generation integer not null default 0,
+ primary key(profile,id));
+
+insert into transactions_new(profile, id, data_hash, year, month, day, description,
+ comment, generation)
+select profile, id, data_hash, year, month, day, description,
+ comment, generation
+from transactions;
+
+-- transaction_accounts
+create table transaction_accounts_new(
+ profile varchar not null,
+ transaction_id integer not null,
+ order_no integer not null,
+ account_name varchar not null,
+ currency varchar not null default '',
+ amount real not null,
+ comment varchar,
+ generation integer not null default 0,
+ primary key(profile, transaction_id, order_no),
+ foreign key (profile,account_name) references accounts(profile,name)
+ on delete cascade on update restrict,
+ foreign key(profile, transaction_id) references transactions(profile,id)
+ on delete cascade on update restrict);
+
+insert into transaction_accounts_new(profile, transaction_id, order_no, account_name,
+ currency, amount, comment, generation)
+select profile, transaction_id, order_no, account_name,
+ currency, amount, comment, generation
+from transaction_accounts;
+
+--currencies
+create table currencies_new(id integer not null primary key, name varchar not null,
+ position varchar not null, has_gap integer not null);
+
+insert into currencies_new(id, name, position, has_gap)
+select id, name, position, has_gap
+from currencies;
+
+
+-- drop originals
+drop table transaction_accounts;
+drop table transactions;
+drop table account_values;
+drop table accounts;
+drop table description_history;
+drop table profiles;
+drop table options;
+drop table currencies;
+
+-- rename new
+alter table options_new rename to options;
+alter table profiles_new rename to profiles;
+alter table accounts_new rename to accounts;
+alter table account_values_new rename to account_values;
+alter table description_history_new rename to description_history;
+alter table transactions_new rename to transactions;
+alter table transaction_accounts_new rename to transaction_accounts;
+alter table currencies_new rename to currencies;
+
+-- indices
+create index fk_tran_acc_prof_acc on transaction_accounts(profile, account_name);
+create unique index un_transactions_data_hash on transactions(profile,data_hash);
+create index idx_transaction_description on transactions(description);
+
+
+-- new tables
+CREATE TABLE templates (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ regular_expression TEXT NOT NULL,
+ test_text TEXT,
+ transaction_description TEXT,
+ transaction_description_match_group INTEGER,
+ transaction_comment TEXT,
+ transaction_comment_match_group INTEGER,
+ date_year INTEGER,
+ date_year_match_group INTEGER,
+ date_month INTEGER,
+ date_month_match_group INTEGER,
+ date_day INTEGER,
+ date_day_match_group INTEGER,
+ is_fallback INTEGER NOT NULL DEFAULT 0);
+CREATE TABLE template_accounts(
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ template_id INTEGER NOT NULL,
+ acc TEXT,
+ position INTEGER NOT NULL,
+ acc_match_group INTEGER,
+ currency INTEGER,
+ currency_match_group INTEGER,
+ amount REAL,
+ amount_match_group INTEGER,
+ comment TEXT,
+ comment_match_group INTEGER,
+ negate_amount INTEGER,
+ FOREIGN KEY(template_id) REFERENCES templates(id) ON UPDATE RESTRICT ON DELETE CASCADE,
+ FOREIGN KEY(currency) REFERENCES currencies(id) ON UPDATE RESTRICT ON DELETE RESTRICT);
+create index fk_template_accounts_template on template_accounts(template_id);
+create index fk_template_accounts_currency on template_accounts(currency);
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 58 to revision 59
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = on;
+
+begin transaction;
+
+-- profiles
+CREATE TABLE profiles_new (
+id INTEGER NOT NULL PRIMARY KEY,
+deprecated_uuid text,
+name TEXT NOT NULL,
+url TEXT NOT NULL,
+use_authentication INTEGER NOT NULL,
+auth_user TEXT,
+auth_password TEXT,
+order_no INTEGER NOT NULL,
+permit_posting INTEGER NOT NULL,
+theme INTEGER NOT NULL DEFAULT -1,
+preferred_accounts_filter TEXT,
+future_dates INTEGER NOT NULL,
+api_version INTEGER NOT NULL,
+show_commodity_by_default INTEGER NOT NULL,
+default_commodity TEXT,
+show_comments_by_default INTEGER NOT NULL DEFAULT 1,
+detected_version_pre_1_19 INTEGER NOT NULL,
+detected_version_major INTEGER NOT NULL,
+detected_version_minor INTEGER NOT NULL);
+
+insert into profiles_new(
+ deprecated_uuid, name, url, use_authentication, auth_user, auth_password,
+ order_no, permit_posting, theme, preferred_accounts_filter, future_dates, api_version,
+ show_commodity_by_default, default_commodity, show_comments_by_default, detected_version_pre_1_19,
+ detected_version_major, detected_version_minor)
+select uuid, name, url, use_authentication, auth_user, auth_password,
+ order_no, permit_posting, theme, preferred_accounts_filter, future_dates, api_version,
+ show_commodity_by_default, default_commodity, show_comments_by_default, detected_version_pre_1_19,
+ detected_version_major, detected_version_minor
+from profiles;
+
+-- accounts
+create table accounts_new(
+id integer primary key not null,
+profile_id integer not null references profiles_new(id) on delete cascade on update restrict,
+level INTEGER NOT NULL,
+name TEXT NOT NULL,
+name_upper TEXT NOT NULL,
+parent_name TEXT,
+expanded INTEGER NOT NULL DEFAULT 1,
+amounts_expanded INTEGER NOT NULL DEFAULT 0,
+generation INTEGER NOT NULL DEFAULT 0);
+
+insert into accounts_new(profile_id, level, name, name_upper, parent_name, expanded, amounts_expanded, generation)
+select p.id, a.level, a.name, a.name_upper, a.parent_name, a.expanded, a.amounts_expanded, a.generation
+from profiles_new p
+join accounts a on a.profile=p.deprecated_uuid;
+
+-- options
+create table options_new(
+name text not null,
+profile_id integer not null,
+value text,
+primary key(profile_id,name));
+
+insert into options_new(name, profile_id, value)
+select o.name, p.id, o.value
+from options o
+join profiles_new p on p.deprecated_uuid = o.profile;
+
+insert into options_new(name, profile_id, value)
+select o.name, 0, o.value
+from options o
+where o.profile='-';
+
+update options_new
+set name='profile_id'
+ , value=(select p.id from profiles_new p where p.deprecated_uuid=value)
+where name='profile_uuid';
+
+update options_new
+set name='profile_id'
+ , value=(select p.id from profiles_new p where p.deprecated_uuid=options_new.value)
+where name='profile_uuid';
+
+-- account_values
+create table account_values_new(
+id integer not null primary key,
+account_id integer not null references accounts_new(id) on delete cascade on update restrict,
+currency text not null default '',
+value real not null,
+generation integer not null default 0);
+
+insert into account_values_new(account_id, currency, value, generation)
+select a.id, av.currency, av.value, av.generation
+from account_values av
+join profiles_new p on p.deprecated_uuid=av.profile
+join accounts_new a on a.profile_id = p.id and a.name = av.account;
+
+-- transactions
+create table transactions_new(
+id integer not null primary key,
+profile_id integer not null references profiles_new(id) on delete cascade on update restrict,
+ledger_id integer not null,
+description text not null,
+year integer not null,
+month integer not null,
+day integer not null,
+comment text,
+data_hash text not null,
+generation integer not null);
+
+insert into transactions_new(profile_id, ledger_id, description, year, month, day, comment, data_hash, generation)
+select p.id, t.id, t.description, t.year, t.month, t.day, t.comment, t.data_hash, t.generation
+from transactions t
+join profiles_new p on p.deprecated_uuid = t.profile;
+
+-- transaction_accounts
+create table transaction_accounts_new(
+ id integer not null primary key,
+ transaction_id integer not null references transactions_new(id) on delete cascade on update restrict,
+ order_no integer not null,
+ account_name text not null,
+ currency text not null default '',
+ amount real not null,
+ comment text,
+ generation integer not null default 0);
+
+insert into transaction_accounts_new(transaction_id, order_no, account_name,
+ currency, amount, comment, generation)
+select t.id, ta.order_no, ta.account_name, ta.currency, ta.amount, ta.comment, ta.generation
+from transaction_accounts ta
+join profiles_new p on ta.profile=p.deprecated_uuid
+join transactions_new t on ta.transaction_id = t.ledger_id and t.profile_id=p.id;
+
+-- table drop/rename
+drop table options;
+alter table options_new rename to options;
+
+drop table account_values;
+alter table account_values_new rename to account_values;
+
+drop table transaction_accounts;
+alter table transaction_accounts_new rename to transaction_accounts;
+
+drop table transactions;
+alter table transactions_new rename to transactions;
+
+drop table accounts;
+alter table accounts_new rename to accounts;
+
+drop table profiles;
+alter table profiles_new rename to profiles;
+
+-- indices
+create index fk_account_profile on accounts(profile_id);
+create unique index un_account_name on accounts(profile_id, name);
+
+create index fk_account_value_acc on account_values(account_id);
+create unique index un_account_values on account_values(account_id, currency);
+
+create index idx_transaction_description on transactions(description);
+create unique index un_transactions_ledger_id on transactions(profile_id, ledger_id);
+create index fk_transaction_profile on transactions(profile_id);
+
+create unique index un_transaction_accounts on transaction_accounts(transaction_id, order_no);
+create index fk_trans_acc_trans on transaction_accounts(transaction_id);
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 58 to revision 59
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = on;
+
+begin transaction;
+
+-- drop description_history, not used any more
+drop table description_history;
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 60 to revision 61
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = on;
+
+begin transaction;
+
+alter table transactions
+add description_uc text not null default '';
+
+update transactions
+set description_uc=upper(description);
+
+delete from options where name='last_scrape';
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 61 to revision 62
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = on;
+
+begin transaction;
+
+delete from currencies
+where id not in (select min(id) from currencies group by name);
+
+create unique index currency_name_idx on currencies(name);
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 62 to revision 63
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = off;
+
+begin transaction;
+
+CREATE TABLE new_templates (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ uuid TEXT NOT NULL,
+ regular_expression TEXT NOT NULL,
+ test_text TEXT,
+ transaction_description TEXT,
+ transaction_description_match_group INTEGER,
+ transaction_comment TEXT,
+ transaction_comment_match_group INTEGER,
+ date_year INTEGER,
+ date_year_match_group INTEGER,
+ date_month INTEGER,
+ date_month_match_group INTEGER,
+ date_day INTEGER,
+ date_day_match_group INTEGER,
+ is_fallback INTEGER NOT NULL DEFAULT 0);
+
+insert into new_templates(id, name, uuid, regular_expression, test_text,
+ transaction_description, transaction_description_match_group,
+ transaction_comment, transaction_comment_match_group,
+ date_year, date_year_match_group,
+ date_month, date_month_match_group,
+ date_day, date_day_match_group,
+ is_fallback)
+select id, name, random(), regular_expression, test_text,
+ transaction_description, transaction_description_match_group,
+ transaction_comment, transaction_comment_match_group,
+ date_year, date_year_match_group,
+ date_month, date_month_match_group,
+ date_day, date_day_match_group,
+ is_fallback
+from templates;
+
+drop table templates;
+alter table new_templates rename to templates;
+
+create unique index templates_uuid_idx on templates(uuid);
\ No newline at end of file
--- /dev/null
+-- Copyright © 2021 Damyan Ivanov.
+-- This file is part of MoLe.
+-- MoLe is free software: you can distribute it and/or modify it
+-- under the term of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your opinion), any later version.
+--
+-- MoLe is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License terms for details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+
+-- migrate from revision 63 to revision 64
+
+-- pragmas need to be outside of transaction control
+-- foreign_keys is needed so that foreign key constraints are redirected
+
+commit transaction;
+pragma foreign_keys = off;
+
+begin transaction;
+
+-- profiles
+CREATE TABLE profiles_new (
+id INTEGER NOT NULL PRIMARY KEY,
+uuid TEXT NOT NULL,
+name TEXT NOT NULL,
+url TEXT NOT NULL,
+use_authentication INTEGER NOT NULL,
+auth_user TEXT,
+auth_password TEXT,
+order_no INTEGER NOT NULL,
+permit_posting INTEGER NOT NULL,
+theme INTEGER NOT NULL DEFAULT -1,
+preferred_accounts_filter TEXT,
+future_dates INTEGER NOT NULL,
+api_version INTEGER NOT NULL,
+show_commodity_by_default INTEGER NOT NULL,
+default_commodity TEXT,
+show_comments_by_default INTEGER NOT NULL DEFAULT 1,
+detected_version_pre_1_19 INTEGER NOT NULL,
+detected_version_major INTEGER NOT NULL,
+detected_version_minor INTEGER NOT NULL);
+
+insert into profiles_new(
+ uuid, name, url, use_authentication, auth_user, auth_password,
+ order_no, permit_posting, theme, preferred_accounts_filter, future_dates, api_version,
+ show_commodity_by_default, default_commodity, show_comments_by_default, detected_version_pre_1_19,
+ detected_version_major, detected_version_minor)
+select coalesce(deprecated_uuid, random()), name, url, use_authentication, auth_user, auth_password,
+ order_no, permit_posting, theme, preferred_accounts_filter, future_dates, api_version,
+ show_commodity_by_default, default_commodity, show_comments_by_default, detected_version_pre_1_19,
+ detected_version_major, detected_version_minor
+from profiles;
+
+drop table profiles;
+alter table profiles_new rename to profiles;
+
+create unique index profiles_uuid_idx on profiles(uuid);
\ No newline at end of file
+++ /dev/null
-create table if not exists accounts(name varchar);
-create index if not exists idx_accounts_name on accounts(name);
-create table if not exists options(name varchar, value varchar);
-create unique index if not exists idx_options_name on options(name);
-create table if not exists account_values(account varchar not null, currency varchar not null, value decimal(18,2) not null);
-create index if not exists idx_account_values_account on account_values(account);
-create unique index if not exists un_account_values on account_values(account,currency);
+++ /dev/null
-alter table accounts add keep boolean;
-alter table account_values add keep boolean;
\ No newline at end of file
+++ /dev/null
-delete from transaction_accounts;
-delete from transactions;
\ No newline at end of file
+++ /dev/null
-create table profiles(uuid varchar not null primary key, name not null, url not null, use_authentication boolean not null, auth_user varchar, auth_password varchar);
-create unique index un_profile_name on profiles(name);
\ No newline at end of file
+++ /dev/null
-drop index un_profile_name;
\ No newline at end of file
+++ /dev/null
-delete from options where name='transaction_list_last_update';
-delete from options where name='last_refresh';
-alter table options add profile varchar;
-drop index idx_options_name;
-create unique index un_options on options(profile,name);
---
-drop table account_values;
-create table account_values(profile varchar not null, account varchar not null, currency varchar not null default '', keep boolean, value decimal not null );
-create unique index un_account_values on account_values(profile,account,currency);
---
-drop table accounts;
-create table accounts(profile varchar not null, name varchar not null, name_upper varchar not null, hidden boolean not null default 0, keep boolean not null default 0, level integer not null, parent_name varchar);
-create unique index un_accounts on accounts(profile, name);
---
-drop table transaction_accounts;
-drop table transactions;
---
-create table transactions(id integer not null, data_hash varchar not null, date varchar not null, description varchar not null, keep boolean not null default 0);
-create unique index un_transactions_id on transactions(id);
-create unique index un_transactions_data_hash on transactions(data_hash);
---
-create table transaction_accounts(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(transaction_id) references transactions(id));
\ No newline at end of file
+++ /dev/null
-drop table transaction_accounts;
-drop table transactions;
---
-create table transactions(profile varchar not null, id integer not null, data_hash varchar not null, date varchar not null, description varchar not null, keep boolean not null default 0);
-create unique index un_transactions_id on transactions(profile,id);
-create unique index un_transactions_data_hash on transactions(profile,data_hash);
---
-create table transaction_accounts(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,account_name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
\ No newline at end of file
+++ /dev/null
-delete from options where profile is null and name='last_scrape';
-create table new_options(profile varchar not null, name varchar not null, value varchar);
-
-insert into new_options(profile, name, value) select distinct '-', o.name, (select o2.value from options o2 where o2.name=o.name and o2.profile is null) from options o where o.profile is null;
-insert into new_options(profile, name, value) select distinct o.profile, o.name, (select o2.value from options o2 where o2.name=o.name and o2.profile=o.profile) from options o where o.profile is not null;
-drop table options;
-create table options(profile varchar not null, name varchar not null, value varchar);
-create unique index un_options on options(profile,name);
-insert into options(profile,name,value) select profile,name,value from new_options;
-drop table new_options;
\ No newline at end of file
+++ /dev/null
-alter table profiles add order_no integer;
\ No newline at end of file
+++ /dev/null
-alter table profiles add permit_posting boolean default 0;
-update profiles set permit_posting = 1;
\ No newline at end of file
+++ /dev/null
-alter table profiles add theme integer default -1;
-update profiles set theme = -1;
\ No newline at end of file
+++ /dev/null
-alter table accounts add expanded default 1;
-update accounts set expanded = 1;
\ No newline at end of file
+++ /dev/null
-create table description_history(description varchar not null primary key, keep boolean);
\ No newline at end of file
+++ /dev/null
-delete from accounts where not exists (select 1 from profiles where uuid = profile);
-delete from account_values where not exists (select 1 from profiles where uuid = profile);
-delete from transactions where not exists (select 1 from profiles where uuid = profile);
-delete from transaction_accounts where not exists (select 1 from profiles where uuid = profile);
\ No newline at end of file
+++ /dev/null
-alter table accounts add amounts_expanded boolean default 0;
\ No newline at end of file
+++ /dev/null
-alter table profiles add preferred_accounts_filter varchar;
\ No newline at end of file
+++ /dev/null
-alter table profiles add future_dates integer;
\ No newline at end of file
+++ /dev/null
-alter table profiles add api_version integer;
\ No newline at end of file
+++ /dev/null
-create table currencies(id integer not null primary key, name varchar not null, position varchar not null, has_gap boolean not null);
\ No newline at end of file
+++ /dev/null
-alter table transaction_accounts add comment varchar;
\ No newline at end of file
+++ /dev/null
-alter table profiles add show_commodity_by_default boolean default 0;
\ No newline at end of file
+++ /dev/null
-alter table profiles add default_commodity varchar;
\ No newline at end of file
+++ /dev/null
-create index idx_transaction_description on transactions(description);
\ No newline at end of file
+++ /dev/null
-alter table description_history add description_upper varchar;
-update description_history set description_upper = upper(description);
-alter table accounts add name_upper varchar;
-update accounts set name_upper = upper(name);
\ No newline at end of file
+++ /dev/null
-delete from options where profile <> '-' and not exists (select 1 from profiles p where p.uuid=options.profile);
\ No newline at end of file
+++ /dev/null
-alter table profiles add show_comments_by_default boolean default 0;
\ No newline at end of file
+++ /dev/null
-update profiles set show_comments_by_default = 1;
\ No newline at end of file
+++ /dev/null
-alter table transactions add comment varchar;
\ No newline at end of file
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-alter table transactions add year integer not null default 0;
-alter table transactions add month integer not null default 0;
-alter table transactions add day integer not null default 0;
-alter table transactions add tmp_md varchar;
-update transactions set year= cast(substr(date, 1,instr(date, '/')-1) as integer);
-update transactions set tmp_md= substr(date, instr(date, '/')+1);
-update transactions set month=cast(substr(tmp_md,1,instr(tmp_md,'/')-1) as integer);
-update transactions set day= cast(substr(tmp_md, instr(tmp_md,'/')+1) as integer);
--- alter table transactions drop date
-create table transactions_2(profile varchar not null, id integer not null, data_hash varchar not null, year integer not null, month integer not null, day integer not null, description varchar not null, comment varchar, keep boolean not null default 0);
-insert into transactions_2(profile, id, data_hash, year, month, day, description, comment, keep) select profile, id, data_hash, year, month, day, description, comment, keep from transactions;
-drop table transactions;
-alter table transactions_2 rename to transactions;
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-create table accounts_new(profile varchar not null, name varchar not null, name_upper varchar not null, keep boolean not null default 0, level integer not null, parent_name varchar, expanded default 1, amounts_expanded boolean default 0);
-insert into accounts_new(profile, name, name_upper, keep, level, parent_name, expanded, amounts_expanded) select profile, name, name_upper, keep, level, parent_name, expanded, amounts_expanded from accounts;
-drop table accounts;
-alter table accounts_new rename to accounts;
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-alter table accounts add generation integer default 0;
-alter table account_values add generation integer default 0;
-alter table transactions add generation integer default 0;
-alter table transaction_accounts add generation integer default 0;
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-alter table transaction_accounts add order_no integer not null default 0;
-update transaction_accounts set order_no = rowid;
-create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-CREATE TABLE transaction_accounts_new(profile varchar not null, transaction_id integer not null, account_name varchar not null, currency varchar not null default '', amount decimal not null, comment varchar, generation integer default 0, order_no integer not null default 0, constraint fk_transaction_accounts_acc foreign key(profile,account_name) references accounts(profile,name), constraint fk_transaction_accounts_trn foreign key(profile, transaction_id) references transactions(profile,id));
-insert into transaction_accounts_new(profile, transaction_id, account_name, currency, amount, comment, generation, order_no) select profile, transaction_id, account_name, currency, amount, comment, generation, order_no from transaction_accounts;
-drop table transaction_accounts;
-alter table transaction_accounts_new rename to transaction_accounts;
-create unique index un_transaction_accounts_order on transaction_accounts(profile, transaction_id, order_no);
\ No newline at end of file
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-create table description_history_new(description varchar not null primary key, description_upper varchar, generation integer default 0);
-insert into description_history_new(description, description_upper) select description, description_upper from description_history;
-drop table description_history;
-alter table description_history_new rename to description_history;
-create unique index un_description_history on description_history(description_upper);
\ No newline at end of file
+++ /dev/null
-alter table accounts add hidden boolean default 0;
-update accounts set hidden = 0;
\ No newline at end of file
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-pragma foreign_keys=off;
-delete from transaction_accounts where not exists (select 1 from accounts a where a.profile=transaction_accounts.profile and a.name=transaction_accounts.account_name);
-delete from transaction_accounts where not exists (select 1 from transactions t where t.profile=transaction_accounts.profile and t.id=transaction_accounts.transaction_id);
\ No newline at end of file
+++ /dev/null
--- Copyright © 2020 Damyan Ivanov.
--- This file is part of MoLe.
--- MoLe is free software: you can distribute it and/or modify it
--- under the term of the GNU General Public License as published by
--- the Free Software Foundation, either version 3 of the License, or
--- (at your opinion), any later version.
---
--- MoLe is distributed in the hope that it will be useful,
--- but WITHOUT ANY WARRANTY; without even the implied warranty of
--- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
--- GNU General Public License terms for details.
---
--- You should have received a copy of the GNU General Public License
--- along with MoLe. If not, see <https://www.gnu.org/licenses/>.
-alter table profiles add detected_version_pre_1_19 boolean;
-alter table profiles add detected_version_major integer;
-alter table profiles add detected_version_minor integer;
\ No newline at end of file
+++ /dev/null
-alter table accounts add level integer;
-alter table accounts add parent varchar;
\ No newline at end of file
+++ /dev/null
-drop index idx_accounts_name;
-create table accounts_tmp(name varchar not null, name_upper varchar not null primary key, hidden boolean not null default 0, level integer not null default 0, parent_name varchar);
-insert or replace into accounts_tmp(name, name_upper, hidden, level, parent_name) select name, name_upper, hidden, level, parent from accounts;
-drop table accounts;
-create table accounts(name varchar not null, name_upper varchar not null primary key, hidden boolean not null default 0, level integer not null default 0, parent_name varchar, keep boolean default 1);
-insert into accounts(name, name_upper, hidden, level, parent_name) select name, name_upper, hidden, level, parent_name from accounts_tmp;
-drop table accounts_tmp;
+++ /dev/null
-create table transactions(id varchar primary key, date varchar, description varchar);
-create table transaction_accounts(transaction_id integer not null, account_name varchar not null, amount float, currency varchar, foreign key (transaction_id) references transactions(id), foreign key(account_name) references accounts(name));
\ No newline at end of file
+++ /dev/null
-alter table transactions add data_hash varchar;
-delete from transactions;
\ No newline at end of file
+++ /dev/null
-alter table transactions add keep boolean default 1 not null;
-update transactions set keep = 1;
-create table transactions_new(id integer, date varchar, description varchar, data_hash varchar, keep boolean);
-insert into transactions_new(id, date, description, data_hash, keep) select cast(id as integer), date, description, data_hash, keep from transactions;
-drop table transactions;
-create table transactions(id integer primary key, date varchar, description varchar, data_hash varchar, keep boolean);
-create unique index un_transactions_data_hash on transactions(data_hash);
-insert into transactions(id, date, description, data_hash, keep) select id, date, description, data_hash, keep from transactions_new;
-drop table transactions_new;
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<!--<item>Expand all</item>-->
<!--<item>Collapse all</item>-->
</string-array>
+ <string-array name="templates_ctx_menu">
+ <item>Промяна</item>
+ <item>Дублиране</item>
+ <item>Изтриване</item>
+ </string-array>
+ <string-array name="template_list_help_text">
+ <item>Макетите са като предварително попълнени движения. Някои от параметрите на движението са указани в макета, а други се извличат от външен източник.</item>
+ <item>Например, при въвеждане на ново движение може да се сканира QR код от касова бележка, което да доведе до автоматично попълване на описанието на движението и имената на сметките от макета и попълване на датата и сумата от данните в QR кода.</item>
+ <item>Макетите описват кои параметри на движението са фиксирани и кои идват от външния източник.</item>
+ <item>Сканирането на QR код е единственият външен източник, който се поддържа в момента. В бъдеще е планирана работа с поставяне на текст от работния буфер и четене/прихващане на текстови съобщения (SMS).</item>
+ </string-array>
+ <string-array name="template_params_help">
+ <item>Шаблонът е регулярен израз ([Уикипедия↗](https://bg.wikipedia.org/wiki/Регулярен_израз#Синтаксис)). При получаване на данни от външния източник, шаблонът се напасва към тях за да се определи кои макети отговарят на входните данни.</item>
+ <item>Поддържа се използването на разпознати групи за попълване на параметри на движението от входните данни.</item>
+ </string-array>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2024 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
-->
<resources>
- <string name="title_activity_settings">Настройки</string>
- <string name="pref_header_backend">Сървър</string>
<string name="pref_title_use_http_auth">Удостоверяване</string>
- <string name="pref_description_use_http_auth_on">Използване на удостоверяване при свързване със сървъра</string>
- <string name="pref_description_use_http_auth_off">Без използване на удостоверяване при свързване със сървъра</string>
- <string name="pref_title_backend_url">Адрес на сървъра</string>
<string name="nav_reports_title">Справки</string>
<string name="nav_transactions_title">Движения</string>
<string name="action_settings">Настройки</string>
<string name="pref_title_backend_auth_user">Потребител</string>
<string name="pref_title_backend_auth_password">Парола</string>
- <string name="title_activity_new_transaction">Ð\9dова Ñ\82Ñ\80анзакÑ\86иÑ\8f</string>
+ <string name="title_activity_new_transaction">Ð\9dово движение</string>
<string name="new_transaction_account_hint">Сметка</string>
<string name="new_transaction_date_hint">днес</string>
- <string name="msg_at_least_two_accounts_are_required">Задължително е използването на поне две сметки</string>
- <string name="err_net_io_error">Мрежова грешка</string>
- <string name="err_bad_backend_url">Неправилен адрес на сървър</string>
- <string name="progress_connecting">Свързване…</string>
<string name="new_transaction_description_hint">Описание</string>
<string name="account_summary_title">Сметки</string>
- <string name="menu_hide_acc_condensed_title">Скриване</string>
- <string name="menu_acc_view_transactions">Преглед на транзакциите</string>
- <string name="menu_acc_summary_refresh_title">Обновяване</string>
- <string name="err_net_error">Мрежова грешка</string>
- <string name="menu_acc_summary_show_only_starred_title">Показване само на любимите</string>
<string name="action_reset_new_transaction_activity_title">Отначало</string>
- <string name="interface_pref_header_title">Интерфейс</string>
- <string name="pref_show_only_starred_off_summary">Списъкът на сметките съдържа всички сметки</string>
- <string name="pref_show_only_starred_on_summary">Показват се само отбелязаните със звезда</string>
- <string name="menu_acc_summary_cancel_selection_title">Отказ</string>
- <string name="err_bad_auth">Грешно потребителско име или парола</string>
- <string name="new_transaction_amount_hint">0,00</string>
- <string name="transactions_last_update_label">Данни към:</string>
- <string name="title_profile_list">Профили</string>
<string name="profiles">Профили</string>
<string name="title_profile_details">Данни за профила</string>
- <string name="transaction_last_update_never">никога</string>
- <string name="err_cancelled">Операцията е прекъсната</string>
- <string name="title_activity_transaction_list">Транзакции</string>
- <string name="err_http_error">Грешка в HTTP</string>
<string name="new_profile_title">Нов профил</string>
<string name="delete_profile">Изтриване на профила</string>
<string name="delete">Изтриване</string>
<item>Ноември</item>
<item>Декември</item>
</string-array>
- <string name="error_invalid_date">Грешна дата</string>
<string name="url_label">Адрес</string>
<string name="profile_name_label">Име на профила</string>
<string name="create_profile_label">Създаване на профил</string>
<string name="err_profile_url_empty">Моля, въведете адрес, например https://server/location</string>
<string name="err_profile_user_name_empty">Въвеждането на потребителско име е задължително когато се използва удостоверяване</string>
<string name="err_profile_password_empty">Паролата е задължителна</string>
- <string name="posting_permitted">Позволяване на добавянето на нови транзакции</string>
+ <string name="posting_permitted">Позволяване на добавянето на нови движения</string>
<string name="send_crash_via">Изпращане на доклада чрез:</string>
<string name="crash_send_question">Желаете ли да изпратите доклад за грешката на автора? Това ще помогне за диагностициране и отстраняване на проблема. Докладът се изпраща по email, като имате възможност за преглед преди изпращане.</string>
<string name="crash_app_condensed_label">Срив</string>
<string name="btn_send_crash_report">Изпращане…</string>
<string name="crash_app_label">Тестов срив</string>
<string name="crash_dialog_title">MoLe се срина</string>
- <string name="crash_report_contents_label">Съдържание на доклада:</string>
<string name="profile_subtitle_read_only">(Само за преглед)</string>
- <string name="menu_acc_summary_confirm_selection_title">Потвърждаване на избора</string>
- <string name="menu_acc_summary_hide_selected_title">Скриване на маркираните сметки</string>
<string name="btn_show_report">Показване на доклада</string>
<string name="btn_select_label">Избор</string>
<string name="profile_color_label">Цвят на профила</string>
<string name="default_color_btn">По подразбиране</string>
- <string name="btn_cancel">Отказ</string>
- <string name="btn_ok">Добре</string>
<string name="profile_list_rearrange_handle_label">Манипулатор за промяна на подредбата</string>
- <string name="color_label">Цвят</string>
- <string name="pref_preferred_autocompletion_account_filter_hint" >Филтър при избор на предишна транзакция</string>
+ <string name="pref_preferred_autocompletion_account_filter_hint">Филтър при избор на предишно движение</string>
<string name="remove_profile_dialog_message">Потвърдете окончателното премахване на профила</string>
<string name="Remove">Премахване</string>
- <string name="text_loading">Зареждане…</string>
<string name="err_invalid_url">Грешен или непълен адрес</string>
<string name="btn_color_picker_button">Бутон за избор на цвят</string>
<string name="insecure_scheme_with_auth">ВНИМАНИЕ: Използване на удостоверяване с несигурна схема на достъп</string>
<string name="future_dates_all">Без ограничения</string>
<string name="future_dates_none">Без въвеждане на бъдещи дати</string>
<string name="profile_future_dates_label">Въвеждане на дати в бъдещето</string>
- <string name="api_auto">Ð\90вÑ\82омаÑ\82иÑ\87но оÑ\82кÑ\80иване</string>
+ <string name="api_auto">Ð\90вÑ\82омаÑ\82иÑ\87на</string>
<string name="api_html">Версия преди 1.14</string>
- <string name="api_post_1_14">Версия 1.15 или по-нова</string>
- <string name="api_pre_1_15">Версия 1.14.x</string>
- <string name="profile_api_version_title">Версия на сървъра</string>
+ <string name="api_1_15">Версия 1.15</string>
+ <string name="api_1_14">Версия 1.14</string>
+ <string name="profile_api_version_title">Версия на протокола</string>
<string name="add_button">Добавяне…</string>
<string name="close_button">Затваряне</string>
<string name="transaction_account_comment_hint">бележка</string>
<string name="show_currency_input">Валута</string>
<string name="currency_input_by_default">Показване по подразбиране на полето за валута</string>
<string name="profile_default_commodity">Валута по подразбиране</string>
- <string name="ignoring_preferred_account">Липсват транзакции с предпочитаната сметка</string>
+ <string name="ignoring_preferred_account">Липсват движения с предпочитаната сметка</string>
<string name="show_comments_switch">Коментари</string>
<string name="show_comment_input_by_default">Показване по подразбиране на полетата за бележки</string>
<string name="icon">икона</string>
<string name="navigation_drawer_open">Отваряне на страничния панел</string>
<string name="navigation_drawer_close">Затваряне на страничния панел</string>
<string name="nav_header_desc">Заглавна част на страничния панел</string>
- <string name="transaction_count_summary">%,d движения към %s</string>
- <string name="account_count_summary">%,d сметки към %s</string>
- <string name="api_version_unknown_label">Неизвестна</string>
- <string name="api_pre_1_19">Преди 1.20.?</string>
- <string name="detected_version_label">Открита версия</string>
+ <string name="transaction_count_summary">%1$,d движения към %2$s</string>
+ <string name="account_count_summary">%1$,d сметки към %2$s</string>
+ <string name="server_version_unknown_label">Неизвестна</string>
+ <string name="detected_server_pre_1_20_1">Преди 1.20.1</string>
+ <string name="new_transaction_fab_description">Знак плюс</string>
+ <string name="api_1_19_1">Версия 1.19.1</string>
+ <string name="profile_server_version_title">Версия на сървъра</string>
+ <string name="err_json_parser_error">Грешка при разчитане на отговора от сървъра. Вероятно настроената врсия на протокола не се поддържа.</string>
+ <string name="btn_profile_options">Настройка на профила</string>
+ <string name="err_json_send_error_head">Грешка при изпращане на движението към сървъра</string>
+ <string name="err_json_send_error_tail">Вероятно настроената версия на протокола не се поддържа.</string>
+ <string name="err_json_send_error_unsupported">Възможно е програмния интерфейс на сървъра да не се поддържа от MoLe</string>
+ <string name="scan_qr">Сканиране на QR код</string>
+ <string name="nav_templates">Макети</string>
+ <string name="title_activity_templates">Макети</string>
+ <string name="help_menu_item_title">Помощ</string>
+ <string name="template_details_account_comment_label">Бележка към сметката</string>
+ <string name="template_details_account_amount_label">Сума</string>
+ <string name="choose_template_detail_source_label">Прихващане от шаблона</string>
+ <string name="missing_pattern_error">Липсва шаблон</string>
+ <string name="missing_test_text">Липсва примерен текст</string>
+ <string name="pattern_without_groups">Шаблонът няма прихващания</string>
+ <string name="pattern_does_not_match">Шаблонът не съвпада с примерния текст</string>
+ <string name="template_transaction_parameters_label">Данни за движението</string>
+ <string name="template_transaction_description_hint">Описание на движението</string>
+ <string name="template_transaction_comment_hint">Бележка към движението</string>
+ <string name="transaction_description_source_label">Източник на описанието на движението</string>
+ <string name="transaction_comment_source_label">Източник на бележката към движението</string>
+ <string name="template_details_date_label">Дата на движението</string>
+ <string name="date_year_hint">година</string>
+ <string name="date_month_hint">месец</string>
+ <string name="date_day_hint">ден</string>
+ <string name="template_details_date_year_source_label">година</string>
+ <string name="template_details_date_day_source_label">ден</string>
+ <string name="month_source_label">месец</string>
+ <string name="unnamed_template">Макет без име</string>
+ <string name="add_button_description">Добавяне на макет</string>
+ <string name="no_template_matches">Няма съвпадение с нито един макет</string>
+ <string name="choose_template_to_apply">Избор на макет</string>
+ <string name="title_edit_template">Промяна на макет</string>
+ <string name="title_new_template">Създаване на макет</string>
+ <string name="pattern_has_errors">Шаблонът съдържа грешки</string>
+ <string name="account_name_is_empty">Липсва сметка</string>
+ <string name="pattern_is_empty">Липсва шаблон</string>
+ <string name="invalid_matching_group_number">Невалиден номер на прихващане</string>
+ <string name="template_name_label">Име на макет</string>
+ <string name="template_details_pattern_label">Шаблон</string>
+ <string name="template_details_test_text_label">Примерен текст</string>
+ <string name="template_details_account_name_label">Сметка</string>
+ <string name="template_details_account_row_label">Данни за сметка №%d</string>
+ <string name="account_name_source_label">Източник на името на сметката</string>
+ <string name="template_details_source_literal">ръчно въвеждане</string>
+ <string name="account_comment_source_label">Източник на бележка към сметката</string>
+ <string name="account_amount_source_label">Източник на сумата</string>
+ <string name="template_xxx_deleted">Макетът „%1$s“ е изтрит</string>
+ <string name="action_undo">Връщане</string>
+ <string name="pattern_match_result">Резултат от прилагането на шаблона</string>
+ <string name="template_item_match_group_source">Група %1$d (%2$s)</string>
+ <string name="template_account_keep_amount_sign">Без промяна на знака</string>
+ <string name="template_account_change_amount_sign">Обръщане на знака на сумата (от плюс на минус и от минус на плюс)</string>
+ <string name="template_account_negate_amount_label">Знак на сумата</string>
+ <string name="template_is_fallback_label">Резервен макет</string>
+ <string name="template_is_fallback_yes">Макетът ще се предлага за избор само ако няма друг макет, който да пасва и да не е маркиран като резервен</string>
+ <string name="template_is_fallback_no">Макетът не е резервен</string>
+ <string name="fallback_templates_divider">Резервни макети</string>
+ <string name="template_list_help_title">Макети</string>
+ <string name="template_details_template_params_label">Параметри на макета</string>
+ <string name="template_params_help_description">Помощна информация за пааметрите на макета</string>
+ <string name="account_currency_source_label">Източник на валутата</string>
+ <string name="action_import_export">Резервно копие</string>
+ <string name="backup_header">Резервно копие</string>
+ <string name="backup_button_label">Създаване</string>
+ <string name="restore_header">Възстановяване от резервно копие</string>
+ <string name="restore_button_label">Възстановяване</string>
+ <string name="backups_activity_label">Резервно копие</string>
+ <string name="backup_explanation">Записване на данните за профили и макети във файл във формат JSON. Това включва и паролите в явен вид. Резервното копие може да се използва за възстановяване на настройките на друго устройство или след нулиране. Данните за сметките и движенията по тях не се записват, а ще бъдат изтеглени от сървъра при възстановяване на настройките.</string>
+ <string name="config_saved">Настройките са записани</string>
+ <string name="restore_explanation">Зареждане на настройките на профилите и макетите от резервно копие, създадено по-рано. Съществуващите записи не се променят. Ако искате да върнете някой запис (профил или макет) към състоянитето от резервното копие, първо го изтрийте.</string>
+ <string name="config_restored">Успешно възстановяване на настройките</string>
+ <string name="no_profile_restore_hint">… а може и да възстановите настройките от резервно копие</string>
+ <string name="profile_not_available">Недостъпен профил</string>
+ <string name="api_1_23">Версия 1.23</string>
+ <string name="accounts_menu_show_zero">Сметки с нулев баланс</string>
+ <string name="accounts_menu_show_zero_condensed">Нулеви сметки</string>
</resources>
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
-->
<resources>
- <dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
\ No newline at end of file
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<!-- Base application theme. -->
<!-- base hue: 261.2245° -->
<!-- target primary color: #935FF2 -->
+ <style name="MoLeAutoCompleteTextViewStyle" parent="Widget.AppCompat.AutoCompleteTextView">
+ <item name="android:popupBackground">#272727</item>
+ </style>
+
+ <style name="MoLeMaterialAutoCompleteTextViewStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu">
+ <item name="android:popupBackground">#272727</item>
+ </style>
+
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorOnPrimary">@android:color/white</item>
- <item name="drawer_background">?android:attr/popupBackground</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="textColor">#ffffff</item>
<item name="textInputStyle">
@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense
</item>
+ <item name="autoCompleteTextViewStyle">@style/MoLeAutoCompleteTextViewStyle</item>
<item name="colorError">#FFE1E2</item>
<item name="colorOnError">#CD1609</item>
<item name="main_header_shadow_height">12dp</item>
<item name="android:textAlignment">viewEnd</item>
</style>
- <style name="account_summary_account_entry_table">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">match_parent</item>
- </style>
-
<style name="transaction_list_comment">
<item name="android:textAppearance">@android:style/TextAppearance.Material.Small</item>
<item name="android:textColor">?commentColor</item>
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<resources>
- <style name="StretchedTextView" parent="Widget.AppCompat.TextView">
- <item name="android:autoSizeTextType">uniform</item>
- </style>
</resources>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<!--<item>Expand all</item>-->
<!--<item>Collapse all</item>-->
</string-array>
+ <string-array name="templates_ctx_menu">
+ <item>Edit</item>
+ <item>Duplicate</item>
+ <item>Delete</item>
+ </string-array>
+ <string-array name="template_list_help_text">
+ <item>Templates are like pre-filled transactions. Some of the transaction parameters are defined by the template, and others can be deduced from external source.</item>
+ <item>For example, when adding a new transaction, you could scan the QR code of a receipt and get the transaction description filled from the template, and date and amounts filled from the data in the QR code.</item>
+ <item>Templates describe which transaction parameters are fixed and which come from the external source.</item>
+ <item>Currently, scanning QR codes is the only available source. Support for pasting from the clipboard and reading/intercepting text messages (SMS) is planned for the future.</item>
+ </string-array>
+ <string-array name="template_params_help">
+ <item>The pattern is a Regular expression ([Wikipedia↗](https://en.wikipedia.org/wiki/Regular_expression#Syntax)). It must match the input from the external source, or the template won\'t be considered when looking for templates corresponding to the input from the external source.</item>
+ <item>Capture groups may be used for filling some transaction parameters.</item>
+ </string-array>
</resources>
\ No newline at end of file
<!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">8dp</dimen>
- <dimen name="nav_header_vertical_spacing">4dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="app_bar_height">200dp</dimen>
- <dimen name="item_width">200dp</dimen>
<dimen name="text_margin">16dp</dimen>
- <dimen name="toolbar_height">56sp</dimen>
+ <dimen name="half_text_margin">8dp</dimen>
+ <dimen name="quarter_text_margin">4dp</dimen>
</resources>
\ No newline at end of file
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2024 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<string name="navigation_drawer_close">Close navigation drawer</string>
<string name="nav_header_desc">Navigation header</string>
<string name="action_settings">Settings</string>
- <string name="nav_header_subtitle" translatable="false">dam+google@ktnx.net</string>
<string name="nav_transactions_title">Transactions</string>
<string name="nav_reports_title">Reports</string>
- <string name="title_activity_settings">Settings</string>
<!-- Strings related to Settings -->
<!-- Example General settings -->
- <string name="pref_header_backend">Backend server</string>
<string name="pref_title_use_http_auth">Enable HTTP authentication</string>
- <string name="pref_description_use_http_auth_on">Use HTTP authentication (basic) when connecting to the backend</string>
- <string name="pref_description_use_http_auth_off">Use plain HTTP without authentication when connecting to the backend</string>
- <string name="pref_title_backend_url">Backend URL</string>
<string name="pref_default_backend_url" translatable="false">https://server/loc</string>
<string name="pref_title_backend_auth_user">Username</string>
<string name="title_activity_new_transaction">New Transaction</string>
<string name="new_transaction_account_hint">Account</string>
<string name="new_transaction_date_hint">today</string>
- <string name="new_transaction_amount_hint">0.00</string>
- <string name="msg_at_least_two_accounts_are_required">At least two accounts are required</string>
- <string name="err_bad_backend_url">Invalid backend URL</string>
- <string name="err_net_io_error">Network I/O error</string>
- <string name="err_http_error">HTTP error</string>
- <string name="err_net_error">Network error</string>
- <string name="progress_connecting">Connecting…</string>
<string name="new_transaction_description_hint">Description</string>
<string name="account_summary_title">Accounts</string>
- <string name="menu_acc_summary_refresh_title">Refresh</string>
- <string name="menu_acc_view_transactions">View transactions</string>
- <string name="menu_hide_acc_condensed_title">Hide</string>
- <string name="menu_acc_summary_show_only_starred_title">Show only starred</string>
- <string name="err_bad_auth">Invalid username or password</string>
<string name="action_reset_new_transaction_activity_title">Reset</string>
- <string name="interface_pref_header_title">Interface</string>
- <string name="pref_show_only_starred_off_summary">Account list contains all accounts</string>
- <string name="pref_show_only_starred_on_summary">Only starred accounts are shown</string>
- <string name="menu_acc_summary_hide_selected_title">Hide selected accounts</string>
- <string name="menu_acc_summary_cancel_selection_title">Cancel selection</string>
- <string name="menu_acc_summary_confirm_selection_title">Confirm selection</string>
- <string name="title_activity_transaction_list">Transactions</string>
- <string name="transactions_last_update_label">Last update:</string>
- <string name="transaction_last_update_never">never</string>
- <string name="err_cancelled">Operation cancelled</string>
- <string name="title_profile_list">Profiles</string>
<string name="title_profile_details">Profile Details</string>
<string name="profiles">Profiles</string>
<string name="new_profile_title" type="id">New profile</string>
<string name="delete_profile">Delete profile</string>
<string name="delete">Delete</string>
- <string name="error_invalid_date">Invalid date</string>
<string name="profile_name_label">Profile name</string>
<string name="url_label">URL</string>
<string name="text_welcome">Welcome</string>
</string-array>
<string name="posting_permitted">Posting of new transactions enabled</string>
<string name="profile_subtitle_read_only">(Read only)</string>
- <string name="crash_report_contents_label">Crash report contents:</string>
<string name="crash_dialog_title">MoLe crashed</string>
<string name="btn_send_crash_report">Send…</string>
<string name="btn_not_now">Not now</string>
<string name="crash_send_question">Would you like to send the crash report to the developer? This would help diagnosing and fixing the problem. The report is sent via email and you can review it before sending.</string>
<string name="send_crash_via">Send crash report via:</string>
<string name="btn_show_report">Show report</string>
- <string name="color_label">Color</string>
<string name="profile_list_rearrange_handle_label">Rearrange items handle</string>
- <string name="btn_ok">OK</string>
- <string name="btn_cancel">Cancel</string>
<string name="default_color_btn">Default</string>
<string name="profile_color_label">Profile color</string>
<string name="btn_select_label">Select</string>
<string name="pref_preferred_autocompletion_account_filter_hint">Filter for transaction auto-completion</string>
<string name="remove_profile_dialog_message">Permanently remove this profile?</string>
<string name="Remove">Remove</string>
- <string name="text_loading">Loading…</string>
<string name="err_invalid_url">Invalid URL</string>
<string name="btn_color_picker_button">Color picker button</string>
<string name="insecure_scheme_with_auth">WARNING: Insecure http used with authentication</string>
<string name="future_dates_365">Up to a year</string>
<string name="future_dates_all">Without restrictions</string>
<string name="api_html">Version before 1.14</string>
- <string name="api_pre_1_15">Version 1.14.x</string>
- <string name="api_post_1_14">Version 1.15 and above</string>
- <string name="api_auto">Detect automatically</string>
- <string name="profile_api_version_title">Backend server version</string>
+ <string name="api_1_14">Version 1.14</string>
+ <string name="api_1_15">Version 1.15</string>
+ <string name="api_auto">Automatic</string>
+ <string name="profile_api_version_title">Protocol version</string>
<string name="currency_symbol" translatable="false">¤</string>
<string name="add_button">Add…</string>
<string name="close_button">Close</string>
<string name="go_to_date_menu_title">Go to date</string>
<string name="splash_icon_description">Main app icon</string>
<string name="sub_accounts_expand_collapse_trigger_description">Sub-accounts expand/collapse trigger</string>
- <string name="transaction_count_summary">%,d transactions as of %s</string>
- <string name="account_count_summary">%,d accounts as of %s</string>
- <string name="api_version_unknown_label">Unknown</string>
- <string name="api_pre_1_19">Before 1.20.?</string>
- <string name="detected_version_label">Detected version</string>
+ <string name="transaction_count_summary">%1$,d transactions as of %2$s</string>
+ <string name="account_count_summary">%1$,d accounts as of %2$s</string>
+ <string name="server_version_unknown_label">Unknown</string>
+ <string name="detected_server_pre_1_20_1">Before 1.20.1</string>
+ <string name="new_transaction_fab_description">Plus icon</string>
+ <string name="api_1_19_1">Version 1.19.1</string>
+ <string name="profile_server_version_title">Server version</string>
+ <string name="err_json_parser_error">Error parsing backend JSON response. Perhaps the configured API version doesn\'t match</string>
+ <string name="btn_profile_options">Configure profile</string>
+ <string name="err_json_send_error_head">Error storing transaction on backend server</string>
+ <string name="err_json_send_error_tail">A mismatch in the configured API version could be causing this</string>
+ <string name="err_json_send_error_unsupported">Perhaps the API of the backend server is not supported by MoLe</string>
+ <string name="scan_qr">Scan QR code</string>
+ <string name="nav_templates">Templates</string>
+ <string name="title_activity_templates">Templates</string>
+ <string name="help_menu_item_title">Help</string>
+ <string name="pattern_has_errors">Pattern has errors</string>
+ <string name="account_name_is_empty">Account name missing</string>
+ <string name="pattern_is_empty">Pattern missing</string>
+ <string name="invalid_matching_group_number">Invalid matching group number</string>
+ <string name="template_name_label">Template name</string>
+ <string name="template_details_pattern_label">Pattern</string>
+ <string name="template_details_test_text_label">Test text</string>
+ <string name="template_details_account_name_label">Account name</string>
+ <string name="template_details_account_row_label">Details of account #%d</string>
+ <string name="account_name_source_label">Account name source</string>
+ <string name="template_details_source_literal">literal</string>
+ <string name="account_comment_source_label">Account comment source</string>
+ <string name="account_amount_source_label">Amount source</string>
+ <string name="template_details_account_comment_label">Account comment</string>
+ <string name="template_details_account_amount_label">Amount</string>
+ <string name="choose_template_detail_source_label">Pattern match group</string>
+ <string name="missing_pattern_error">Missing pattern</string>
+ <string name="missing_test_text">Missing test text</string>
+ <string name="pattern_without_groups">Pattern has no capturing groups</string>
+ <string name="pattern_does_not_match">Pattern doesn\'t match the test text</string>
+ <string name="template_transaction_parameters_label">Transaction parameters</string>
+ <string name="template_transaction_description_hint">Transaction description</string>
+ <string name="template_transaction_comment_hint">Transaction comment</string>
+ <string name="transaction_description_source_label">Transaction description source</string>
+ <string name="transaction_comment_source_label">Transaction comment source</string>
+ <string name="template_details_date_label">Transaction date</string>
+ <string name="date_year_hint">year</string>
+ <string name="date_month_hint">month</string>
+ <string name="date_day_hint">date</string>
+ <string name="template_details_date_year_source_label">year</string>
+ <string name="template_details_date_day_source_label">date</string>
+ <string name="month_source_label">month</string>
+ <string name="unnamed_template">Template with no name</string>
+ <string name="add_button_description">Add template</string>
+ <string name="no_template_matches">No template matches</string>
+ <string name="choose_template_to_apply">Choose template to apply</string>
+ <string name="title_edit_template">Edit template</string>
+ <string name="title_new_template">New template</string>
+ <string name="template_xxx_deleted">Template \'%1$s\' deleted</string>
+ <string name="action_undo">Undo</string>
+ <string name="pattern_match_result">Pattern match result</string>
+ <string name="template_item_match_group_source">Group %1$d (%2$s)</string>
+ <string name="template_account_keep_amount_sign">Sign will not be altered</string>
+ <string name="template_account_change_amount_sign">Amount sign will be changed (plus to minus; minus to plus)</string>
+ <string name="template_account_negate_amount_label">Change amount sign</string>
+ <string name="template_is_fallback_label">Fallback template</string>
+ <string name="template_is_fallback_yes">Template will be offered for selection only when no templates match that aren\'t marked as fallback templates</string>
+ <string name="template_is_fallback_no">Template is a primary, high priority one, not a catch-all</string>
+ <string name="fallback_templates_divider">Fallback templates</string>
+ <string name="template_list_help_title">Templates</string>
+ <string name="template_details_template_params_label">Template parameters</string>
+ <string name="template_params_help_description">Show help on template parameters</string>
+ <string name="account_currency_source_label">Commodity source</string>
+ <string name="action_import_export">Backup/Restore</string>
+ <string name="backup_header">Backup</string>
+ <string name="backup_explanation">Exports all profile configuration and templates to a JSON file. this includes passwords in clear text. Tha backup can later be used to restore the settings on a different device or after a device reset. Data about transactions and accounts are not exported. Instead, these will be fetched from the remote backend when the configuration is restored.</string>
+ <string name="backup_button_label">Backup</string>
+ <string name="restore_header">Restore</string>
+ <string name="restore_explanation">Restores all profiles and templates from a previous backup. Entries that already exist are kept without changes. If you want to restore some entry to a previous state remove it first.</string>
+ <string name="restore_button_label">Restore</string>
+ <string name="config_saved">Configuration saved successfully</string>
+ <string name="backups_activity_label">Backup / Restore</string>
+ <string name="config_restored">Configuration restored successfully</string>
+ <string name="no_profile_restore_hint">… or, you may restore from backup</string>
+ <string name="profile_not_available">Profile not available</string>
+ <string name="api_1_23">Version 1.23</string>
+ <string name="accounts_menu_show_zero">Show zero balances</string>
+ <string name="accounts_menu_show_zero_condensed">Zero balances</string>
</resources>
<!--
- ~ Copyright © 2020 Damyan Ivanov.
+ ~ Copyright © 2021 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
<!-- Base application theme. -->
<!-- base hue: 261.2245° -->
<!-- target primary color: #935FF2 -->
+ <style name="MoLeMaterialAutoCompleteTextViewStyle" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"></style>
+
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorOnPrimary">@android:color/white</item>
- <item name="drawer_background">?android:attr/popupBackground</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="textColor">#686868</item>
<item name="textInputStyle">
@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense
</item>
- <item name="colorError">#FFE1E2</item>
- <item name="colorOnError">#CD1609</item>
+ <item name="colorError">#CD1609</item>
+ <item name="colorOnError">#FFE1E2</item>
<item name="colorPrimary">#935ff2</item>
<item name="colorPrimaryTransparent">#00935ff2</item>
<item name="colorSecondary">#6920ed</item>
<item name="android:textAlignment">viewEnd</item>
</style>
- <style name="account_summary_account_entry_table">
- <item name="android:layout_width">match_parent</item>
- <item name="android:layout_height">match_parent</item>
- </style>
-
<style name="transaction_list_comment">
<item name="android:textAppearance">@android:style/TextAppearance.Material.Small</item>
<item name="android:textColor">?commentColor</item>
+++ /dev/null
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright © 2019 Damyan Ivanov.
- ~ This file is part of MoLe.
- ~ MoLe is free software: you can distribute it and/or modify it
- ~ under the term of the GNU General Public License as published by
- ~ the Free Software Foundation, either version 3 of the License, or
- ~ (at your opinion), any later version.
- ~
- ~ MoLe is distributed in the hope that it will be useful,
- ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
- ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ~ GNU General Public License terms for details.
- ~
- ~ You should have received a copy of the GNU General Public License
- ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- -->
-
-<full-backup-content>
- <!-- Exclude specific shared preferences that contain GCM registration Id -->
-</full-backup-content>
<?xml version="1.0" encoding="utf-8"?><!--
- ~ Copyright © 2019 Damyan Ivanov.
+ ~ Copyright © 2024 Damyan Ivanov.
~ This file is part of MoLe.
~ MoLe is free software: you can distribute it and/or modify it
~ under the term of the GNU General Public License as published by
-->
<network-security-config>
- <base-config cleartextTrafficPermitted="true" />
+ <base-config cleartextTrafficPermitted="true">
+ <trust-anchors>
+ <certificates src="system" />
+ <certificates src="user" />
+ </trust-anchors>
+ </base-config>
</network-security-config>
\ No newline at end of file
+++ /dev/null
-/*
- * Copyright © 2020 Damyan Ivanov.
- * This file is part of MoLe.
- * MoLe is free software: you can distribute it and/or modify it
- * under the term of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your opinion), any later version.
- *
- * MoLe is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License terms for details.
- *
- * You should have received a copy of the GNU General Public License
- * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
- */
-
-package net.ktnx.mobileledger.model;
-
-import org.junit.Test;
-import org.junit.internal.ArrayComparisonFailure;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertThrows;
-
-public class MobileLedgerProfileTest {
- private List<LedgerAccount> listFromArray(LedgerAccount[] array) {
- ArrayList<LedgerAccount> result = new ArrayList<>();
- Collections.addAll(result, array);
-
- return result;
- }
- private void aTest(LedgerAccount[] oldList, LedgerAccount[] newList,
- LedgerAccount[] expectedResult) {
- List<LedgerAccount> result =
- MobileLedgerProfile.mergeAccountListsFromWeb(listFromArray(oldList),
- listFromArray(newList));
- assertArrayEquals(expectedResult, result.toArray());
- }
- private void negTest(LedgerAccount[] oldList, LedgerAccount[] newList,
- LedgerAccount[] expectedResult) {
- List<LedgerAccount> result =
- MobileLedgerProfile.mergeAccountListsFromWeb(listFromArray(oldList),
- listFromArray(newList));
- assertThrows(ArrayComparisonFailure.class,
- () -> assertArrayEquals(expectedResult, result.toArray()));
- }
- private LedgerAccount[] emptyArray() {
- return new LedgerAccount[]{};
- }
- @Test
- public void mergeEmptyLists() {
- aTest(emptyArray(), emptyArray(), emptyArray());
- }
- @Test
- public void mergeIntoEmptyLists() {
- LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null);
- aTest(emptyArray(), new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1});
- }
- @Test
- public void mergeEmptyList() {
- LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null);
- aTest(new LedgerAccount[]{acc1}, emptyArray(), emptyArray());
- }
- @Test
- public void mergeEqualLists() {
- LedgerAccount acc1 = new LedgerAccount(null, "Acc1", null);
- aTest(new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1}, new LedgerAccount[]{acc1});
- }
- @Test
- public void mergeFlags() {
- LedgerAccount acc1a = new LedgerAccount(null, "Acc1", null);
- LedgerAccount acc1b = new LedgerAccount(null, "Acc1", null);
- acc1b.setExpanded(true);
- acc1b.setAmountsExpanded(true);
- List<LedgerAccount> merged = MobileLedgerProfile.mergeAccountListsFromWeb(
- listFromArray(new LedgerAccount[]{acc1a}),
- listFromArray(new LedgerAccount[]{acc1b}));
- assertArrayEquals(new LedgerAccount[]{acc1b}, merged.toArray());
- assertSame(merged.get(0), acc1a);
- // restore original values, modified by the merge
- acc1a.setExpanded(false);
- acc1a.setAmountsExpanded(false);
- negTest(new LedgerAccount[]{acc1a}, new LedgerAccount[]{acc1b},
- new LedgerAccount[]{new LedgerAccount(null, "Acc1", null)});
- }
-}
\ No newline at end of file
/*
- * Copyright © 2020 Damyan Ivanov.
+ * Copyright © 2022 Damyan Ivanov.
* This file is part of MoLe.
* MoLe is free software: you can distribute it and/or modify it
* under the term of the GNU General Public License as published by
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'com.android.tools.build:gradle:8.0.2'
// NOTE: Do not place your application dependencies here; they belong
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
#
-# Copyright © 2019 Damyan Ivanov.
+# Copyright © 2024 Damyan Ivanov.
# This file is part of MoLe.
# MoLe is free software: you can distribute it and/or modify it
# under the term of the GNU General Public License as published by
# along with MoLe. If not, see <https://www.gnu.org/licenses/>.
#
-# Project-wide Gradle settings.
-# IDE (e.g. Android Studio) users:
-# Gradle settings configured through the IDE *will override*
-# any settings specified in this file.
-# For more details on how to configure your build environment visit
+## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
+#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs=-Xmx1536m
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
+#Sun Mar 17 11:29:00 EET 2024
android.debug.obsoleteApi=true
+android.defaults.buildfeatures.buildconfig=true
+android.enableJetifier=false
+android.enableR8.fullMode=false
+android.nonFinalResIds=false
+android.nonTransitiveRClass=true
android.useAndroidX=true
-android.enableJetifier=true
\ No newline at end of file
+org.gradle.jvmargs=-Xmx1024M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
+org.gradle.unsafe.configuration-cache=true
-#Mon Jun 08 21:33:09 EEST 2020
+#
+# Copyright © 2024 Damyan Ivanov.
+# This file is part of MoLe.
+# MoLe is free software: you can distribute it and/or modify it
+# under the term of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your opinion), any later version.
+#
+# MoLe is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License terms for details.
+#
+# You should have received a copy of the GNU General Public License
+# along with MoLe. If not, see <https://www.gnu.org/licenses/>.
+#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windows variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
--- /dev/null
+
+* НОВО
+ + поддръжка на последната версия на протокола за комуникация (hledger-web 1.19.1)
+ + откриване на версията на сървъра
+ + поддръжка на повече от една версия на протокола за комуникация
+* ПОДОБРЕНО
+ + настройките на базата данни се извършват във фонов режим, докато се показва началния екран
+* ПОПРАВЕНО
+ + използване на валутата по подразбиране при въвеждане на ново движение
+ + отстранени няколко срива
--- /dev/null
+
+* NEW
+ + макети за движения, прилагани чрез сканиране на QR код
+* IMPROVEMENTS
+ + по-голям бутон за валута при новите движения
+ + унифицирано поведение на хвърчащия бутон за добавяне/съхраняване
+ + частична миграция към напълно асинхронен слой за връзка с базата данни
--- /dev/null
+
+* ПОПРАВКИ
+ + отстранен проблем при миграцията на данните при профили без проверена версия на сървъра
--- /dev/null
+
+* НОВО
+ + новите движения са видими в списъка с движения без да е нужно презареждане
+* ПОДОБРЕНО
+ + завършена миграция към напълно асинхронен слой за връзка с базата данни
+ + подобрено поведение при превключване от списъка със сметки към списъка с движения за пръв път
+* ПОПРАВЕНО
+ + визуални проблеми в редактора на шаблони
+ + поправена обработка на грешки при изпробване на различни версии на протокола за връзка с hledger-web
+ + без изчистване на датата при зареждане на старо движение
+ + няколко по-малки поправки
--- /dev/null
+
+* НОВОСТИ
+ + поддръжка на валута в шаблоните
+ + баланс с натрупване при филтриране на движенията по сметка
+ + текущ баланс при избор на сметка (ново движение)
+* ПОДОБРЕНИЯ
+ + по-отчетлив фон на изскачащия прозорец при избор на сметка при тъмна тема
+ + подобрено оформление на балансите про много дълги имена на сметки
+* ПОПРАВКИ
+ + зачитане на валутата по подразбиране при въвеждане на ново движение
+ + отразяване на промените в текущо активния профил
+ + поправено разнасяне на новите движения по родителските сметки
--- /dev/null
+* ПОПРАВКИ
+ + отстранен проблем при ново движение и въвеждане на невалидна сума
+ + поправено зареждане на предишно движение по описание (отново)
+ + отстранен срив при разпознаване на версия на hledger, състояща се само от два компонента
--- /dev/null
+* ПОПРАВКИ
+ + поправено намиране на стари движения при въвеждане на описание на ново движение на някои варианти/версии на Андроид (повредено във версия 0.18.0)
--- /dev/null
+* НОВО
+ + съхраняване на резервни копия на всички профили и шаблони; възстановяване от резервно копие
+* ПОПРАВКИ
+ + няколко срива свързани със стартиране на екрана за ново двишение от пряк път
--- /dev/null
+* ПОПРАВКИ
+ + Ново движение: преместване на фокуса в полето за сума след избиране на сметка
+ + Ново движение: отстранен срив при връщане в приложението когато няма фокусиран елемент
+ + отстранен срив при обновяване на БД до версия 0.20.0
+ + поправено възстановяване на настройките при налични празни стойности
+ + без използване на AsyncTask -- вече не се препоръчва
--- /dev/null
+* НОВО
+ + архивно копие в облака
+* ПОПРАВКИ
+ + два проблема в работата на базата данни, единият причиняващ срив при стартиране
--- /dev/null
+* ПОПРАВКИ
+ + отстранен още един проблем при обновяване на БД от версия 0.16.0
--- /dev/null
+* ИЗВЕСТНИ ПРОБЛЕМИ
+ + Несъвместимост с hledger-web 1.23+
+* ПОПРАВКИ
+ + поправено дописване на описанието при въвеждане на ново движение
--- /dev/null
+* НОВО
+ + Добавена поддръжка на hledger-web версия 1.23
+* ПОПРАВКИ
+ + Включване на поддържащ файл за работата на БД, пропъснат във версия 0.20.4
--- /dev/null
+* ПОПРАВКИ
+ + поддръжане на hledger-web 1.23 и при добавяне на нови движения
+ + поправена натрупана сума при добавяне на движение в миналото
+ + поправен срив при изпращане на ново движение без данни за суми
--- /dev/null
+* ПОПРАВКИ
+ + отстранен срив при балансиране на трансакция с повече от една валута
+ + отстранен срив при дублиране на макет
+ + отстранен срив при зареждане на настройки от резервно копие
+* ПОДОБРЕНИЯ
+ + ново движение: включване на полазването на валути при зареждане на предишни движение с валути
--- /dev/null
+* ПОПРАВКИ
+ + коригирани версии на gradle
+* ДРУГИ
+ + обновени версии на множество библиотеки
+ + прицелване във версия 31 на платформата
--- /dev/null
+* ПОПРАВКИ
+ + отстранен проблем със съвместимостта с hledger-web 1.23+ при изпращане на нови транзакции. Благодарности на Faye Duxovni за поправката!
+ + поправен срив при изтриване на шаблони
+ + поправен рядък срив при изпращане на транзакции, съдържащи повече от една сметка без сума и нулев остатъчен баланс
--- /dev/null
+* ПОПРАВКИ
+ + отстранен проблем с централизираното резервно копие на настройките
--- /dev/null
+* ПОПРАВКИ
+ + отстранен проблем при изпращане на транзакции към hledger-web 1.23+
--- /dev/null
+* ПОПРАВКИ
+ + Позволяване на потребителски сертификати в настройките за сигурността на мрежовите връзки
+* ДРУГИ
+ + Обновена версия на gradle
hledger-web е уеб интерфейс за hledger - система за двустранно счетоводство, базирана на текстови файлове.
-MoLe (оÑ\82 "Mobile Ledger" - мобилен Ñ\81Ñ\87еÑ\82оводен жÑ\83Ñ\80нал) пÑ\80едлага по-еÑ\81Ñ\82еÑ\81Ñ\82вен наÑ\87ин на Ñ\80абоÑ\82а Ñ\81 hledger-web за мобилни устройства.
+MoLe (оÑ\82 "Mobile Ledger" - мобилен Ñ\81Ñ\87еÑ\82оводен жÑ\83Ñ\80нал) пÑ\80едлага по-еÑ\81Ñ\82еÑ\81Ñ\82вен наÑ\87ин на Ñ\80абоÑ\82а Ñ\81 hledger-web оÑ\82 мобилни устройства.
Функции:
-<ul><li>СпиÑ\81Ñ\8aк на Ñ\81меÑ\82киÑ\82е Ñ\81 Ñ\82екÑ\83Ñ\89о Ñ\81алдо</li><li>СпиÑ\81Ñ\8aк на движениÑ\8fÑ\82а по Ñ\81меÑ\82киÑ\82е Ñ\81 Ñ\84илÑ\82Ñ\8aÑ\80 по име на Ñ\81меÑ\82ка</li><li>Ð\92Ñ\8aвеждане на ново движение по Ñ\81меÑ\82ка</li><li>РабоÑ\82а Ñ\81 валÑ\83Ñ\82и</li><li>Ð\9cножеÑ\81Ñ\82во изÑ\82оÑ\87ниÑ\86и на данни, в Ñ\86вÑ\8fÑ\82 по желание</li><li>Ð\98денÑ\82иÑ\84иÑ\86иÑ\80ане пÑ\80ед изÑ\82оÑ\87ника на данни</li></ul>
+<ul><li>СпиÑ\81Ñ\8aк на Ñ\81меÑ\82киÑ\82е Ñ\81 Ñ\82екÑ\83Ñ\89о Ñ\81алдо</li><li>СпиÑ\81Ñ\8aк на движениÑ\8fÑ\82а по Ñ\81меÑ\82киÑ\82е Ñ\81 Ñ\84илÑ\82Ñ\8aÑ\80 по име на Ñ\81меÑ\82ка</li><li>Ð\92Ñ\8aвеждане на ново движение по Ñ\81меÑ\82ка</li><li>РабоÑ\82а Ñ\81 валÑ\83Ñ\82и</li><li>Ð\97абележки кÑ\8aм движениÑ\8fÑ\82а по Ñ\81меÑ\82ки и кÑ\8aм оÑ\82делни пеÑ\80а</li><li>Ð\9cножеÑ\81Ñ\82во изÑ\82оÑ\87ниÑ\86и на данни, в Ñ\86вÑ\8fÑ\82 по желание</li><li>Ð\9cакеÑ\82и на движениÑ\8f по Ñ\81меÑ\82ки, акÑ\82ивиÑ\80ани Ñ\87Ñ\80ез Ñ\81каниÑ\80ане на QR код</li></ul>
Приложението е в процес на разработка. Ето някои от планираните функции:
-<ul><li>СпÑ\80авки</li><li>Ð\9fовеÑ\87е Ñ\84илÑ\82Ñ\80и на Ñ\81пиÑ\81Ñ\8aка Ñ\81 движениÑ\8fÑ\82а по Ñ\81меÑ\82киÑ\82е, Ñ\82Ñ\8aÑ\80Ñ\81ене</li><li>Ð\9fопÑ\8aлване на Ñ\84оÑ\80мÑ\83лÑ\8fÑ\80а за ново движение по Ñ\81меÑ\82ка Ñ\81 данни оÑ\82 SMS (напÑ\80имеÑ\80 Ñ\81Ñ\8aобÑ\89ение оÑ\82 банкаÑ\82а) или двÑ\83измеÑ\80ен баÑ\80код</li></ul>
+<ul><li>СпÑ\80авки</li><li>Ð\9fовеÑ\87е Ñ\84илÑ\82Ñ\80и на Ñ\81пиÑ\81Ñ\8aка Ñ\81 движениÑ\8fÑ\82а по Ñ\81меÑ\82киÑ\82е, Ñ\82Ñ\8aÑ\80Ñ\81ене</li><li>Ð\90кÑ\82ивиÑ\80ане на макеÑ\82и на движениÑ\8f по Ñ\81меÑ\82ки Ñ\81 данни оÑ\82 SMS (напÑ\80имеÑ\80 Ñ\81Ñ\8aобÑ\89ение оÑ\82 банкаÑ\82а) или Ñ\80абоÑ\82ниÑ\8f бÑ\83Ñ\84еÑ\80</li></ul>
--- /dev/null
+
+* NEW
+ + add support for latest JSON API (hledger-web 1.19.1)
+ + backend server version detection
+ + backend communication supports multiple JSON API versions
+* IMPROVEMENTS
+ + do database-related initialization in the background while the splash screen is shown
+* FIXES
+ + honour default currency in new transaction entry
+ + several crashes fixed
--- /dev/null
+
+* NEW
+ + transaction templates, applied via QR scan
+* IMPROVEMENTS
+ + bigger commodify button in new transaction screen
+ + unified floating action button behaviour
+ + start migration to a fully asynchronous database layer
--- /dev/null
+
+* FIXES
+ + fix a bug in db migration for profiles without detected version
--- /dev/null
+
+* NEW
+ + newly added transactions are visible in transaction list without a refresh
+* IMPROVEMENTS
+ + finished migration to fully asynchronous database layer
+ + better responsiveness when switching from the account list to the transaction list for the first time
+* FIXES
+ + fix layout glitches in template editor
+ + fix error handling while trying different JSON API versions
+ + stop resetting the date when an old transaction is loaded
+ + several smaller fixes
--- /dev/null
+
+* NEW
+ + add commodity support to the templates
+ + display running totals when filtering transaction list by account
+ + show current balance in account chooser (new transactions)
+* IMPROVEMENTS
+ + more prominent background for auto-complete pop-ups in dark mode
+ + better placement of account balances with very long/deep account names
+* FIXES
+ + honor default commodity setting in new transaction screen
+ + honor changes in currently active profile
+ + fix propagation of speculative account updates to parent accounts
--- /dev/null
+* FIXES
+ + fix a bug in new transaction screen when an invalid amount is entered
+ + fix loading a previous transaction by description (again)
+ + fix crash when parsing of hledger version with only two components
--- /dev/null
+* FIXES
+ + fix auto-completion of transaction names with non-ASCII characters on some Android variants/versions (broken in 0.18.0)
--- /dev/null
+* NEW
+ + backup/restore of profile/template configuration to a file
+* FIXES
+ + fix a couple of crashes related to starting new transaction via shortcut
--- /dev/null
+* FIXES
+ + New transaction: focus amount upon account selection
+ + New transaction: fix a crash when returning to the activity with no focused input field
+ + fix a crash in DB upgrade introduced in v0.20.0
+ + fix config restore with null values
+ + move away from deprecated AsyncTask
--- /dev/null
+* NEW
+ + cloud backup
+* FIXES
+ + two database problems fixed, one causing crashes at startup
--- /dev/null
+* FIXES
+ + another fix to DB migration from v0.16.0
--- /dev/null
+* KNOWN PROBLEMS
+ + Incompatibility with hledger-web 1.23+
+* FIXES
+ + fix auto-completion of transaction description
--- /dev/null
+* NEW
+ + Add support for hledger-web 1.23
+* FIXES
+ + Ship database support file missed in v0.20.4
--- /dev/null
+* FIXES
+ + add hledger-web 1.23 support when adding transactions too
+ + correct running total when a matching transaction is added in the past
+ + fix crash when sending transaction containing only empty amounts
--- /dev/null
+* FIXES
+ + fix crash when auto-balancing multi currency transaction
+ + fix crash when duplicating template
+ + fix crash when restoring configuration backup
+* IMPROVEMENTS
+ + new transaction: turn on commodity setting when loading previous transaction with commodities
--- /dev/null
+* FIXES
+ + sync gradle version requirements
+* OTHERS
+ + bump version of several dependent libraries
+ + bump SDK version to 31
+ + adjust deprecated constructor usage
--- /dev/null
+* FIXES
+ + fix compatibility wuth hledger-web 1.23+ when submitting new transactions. Thanks to Faye Duxovni for the patch!
+ + fix a crash when deleting templates
+ + fix a rare crash when submitting transactions with multiple accounts with no amounts with zero remaining balance
--- /dev/null
+* FIXES
+ + fix cloud backup
--- /dev/null
+* FIXES:
+ + fixed sending of transactions to hledger-web 1.23+
--- /dev/null
+* FIXES:
+ + allow user certificates in network security config
+* OTHERS:
+ + bump gradle version
Features:
-<ul><li>List of accounts with their current balance</li><li>Transaction list with filters</li><li>Input of new transactions</li><li>Currency/commodity support</li><li>Multiple back-ends, optionally with custom color</li><li>Supports http authentication (basic)</li></ul>
+<ul><li>Hierarchical list of accounts with their current balance</li><li>Transaction list with filters</li><li>Input of new transactions</li><li>Currency/commodity support</li><li>Per-transaction and per-posting comments</li><li>Multiple back-ends, optionally with custom color</li><li>Transaction templates matched by QR code</li></ul>
The development is still ongoing, here's a list of some of the planned features:
-<ul><li>Reports</li><li>More filters for the transaction list, search</li><li>Pre-filling of new transaction input from SMS (e.g. from your bank) or QR-code</li></ul>
+<ul><li>Reports</li><li>More filters for the transaction list, search</li><li>Match transaction templates from clipboard or SMS (e.g. from your bank)</li></ul>