]> git.ktnx.net Git - mobile-ledger.git/commitdiff
more pronounced day/month delimiters in the transaction list master
authorDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 31 Mar 2024 11:13:26 +0000 (14:13 +0300)
committerDamyan Ivanov <dam+mobileledger@ktnx.net>
Sun, 31 Mar 2024 11:13:26 +0000 (14:13 +0300)
540 files changed:
CHANGES.md
README.md
TODO.txt [new file with mode: 0644]
app/build.gradle
app/schemas/net.ktnx.mobileledger.db.DB/1.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/43.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/44.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/45.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/46.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/47.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/48.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/49.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/50.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/51.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/52.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/53.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/54.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/55.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/56.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/57.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/58.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/59.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/60.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/61.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/62.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/63.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/64.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/65.json [new file with mode: 0644]
app/schemas/net.ktnx.mobileledger.db.DB/66.json [new file with mode: 0644]
app/src/main/AndroidManifest.xml
app/src/main/ic_launcher-playstore.png [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/App.java
app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTaskParams.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/DbOpItem.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/DbOpQueue.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/DbOpRunner.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/DescriptionSelectedCallback.java
app/src/main/java/net/ktnx/mobileledger/async/GeneralBackgroundTasks.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/RetrieveTransactionsTask.java
app/src/main/java/net/ktnx/mobileledger/async/SendTransactionTask.java
app/src/main/java/net/ktnx/mobileledger/async/TaskCallback.java
app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/TransactionDateFinder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/async/UpdateAccountsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigReader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/AccountValueDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/AsyncResultCallback.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/BaseDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/OptionDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/TemplateAccountDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/Account.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/AccountValue.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmounts.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmountsAutocompleteAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/Currency.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/DB.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/Option.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/Profile.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/Transaction.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TransactionDescriptionAutocompleteAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/db/TransactionWithAccounts.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/err/HTTPException.java
app/src/main/java/net/ktnx/mobileledger/json/API.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/AccountListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ApiNotSupportedException.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/Gateway.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedBalance.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedLedgerAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedPosting.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedPrice.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedQuantity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/ParsedStyle.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/TransactionListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_14/AccountListParser.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/Gateway.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedBalance.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedLedgerAccount.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedLedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPosting.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedPrice.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedQuantity.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/ParsedStyle.java
app/src/main/java/net/ktnx/mobileledger/json/v1_14/TransactionListParser.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/AccountListParser.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/Gateway.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedBalance.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedLedgerAccount.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedLedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPosting.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedPrice.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedQuantity.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/ParsedStyle.java
app/src/main/java/net/ktnx/mobileledger/json/v1_15/TransactionListParser.java
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/AccountListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/Gateway.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedAmount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedBalance.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerTransaction.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPosting.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrecision.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrice.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedQuantity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedSourcePos.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedStyle.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/TransactionListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/AccountListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/Gateway.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedAmount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedBalance.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerAccount.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerTransaction.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPosting.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPrice.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedQuantity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedSourcePos.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedStyle.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/json/v1_23/TransactionListParser.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/Currency.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/Data.java
app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/InertMutableLiveData.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/LedgerAccount.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerAmount.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransaction.java
app/src/main/java/net/ktnx/mobileledger/model/LedgerTransactionAccount.java
app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/model/TransactionListItem.java
app/src/main/java/net/ktnx/mobileledger/ui/AutoCompleteTextViewWithClear.java
app/src/main/java/net/ktnx/mobileledger/ui/CrashReportDialogFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/DatePickerFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/EditTextWithClear.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/HelpDialog.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/HueRing.java
app/src/main/java/net/ktnx/mobileledger/ui/HueRingDialog.java
app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/MobileLedgerListFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/QR.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/QRScanCapableFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/RecyclerItemListener.java
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/TextViewClearHelper.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/CrashReportingActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/MainActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileThemedActivity.java
app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfilesRecyclerViewAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListDivider.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListAdapter.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListDelimiterRowHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListFragment.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListLastUpdateRowHolder.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolder.java
app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolderBase.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/Colors.java
app/src/main/java/net/ktnx/mobileledger/utils/Digest.java
app/src/main/java/net/ktnx/mobileledger/utils/DimensionUtils.java
app/src/main/java/net/ktnx/mobileledger/utils/Globals.java
app/src/main/java/net/ktnx/mobileledger/utils/Locker.java
app/src/main/java/net/ktnx/mobileledger/utils/Logger.java
app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/utils/Misc.java
app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java [deleted file]
app/src/main/java/net/ktnx/mobileledger/utils/NetworkUtil.java
app/src/main/java/net/ktnx/mobileledger/utils/ObservableAtomicInteger.java
app/src/main/java/net/ktnx/mobileledger/utils/ObservableList.java
app/src/main/java/net/ktnx/mobileledger/utils/ObservableValue.java
app/src/main/java/net/ktnx/mobileledger/utils/Profiler.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java [new file with mode: 0644]
app/src/main/java/net/ktnx/mobileledger/utils/UrlEncodedFormData.java
app/src/main/res/anim/fade_in_slowly.xml [new file with mode: 0644]
app/src/main/res/anim/fade_out_slowly.xml [new file with mode: 0644]
app/src/main/res/anim/layout_slide_down.xml [deleted file]
app/src/main/res/anim/rotate_180.xml [deleted file]
app/src/main/res/anim/rotate_180_back.xml [deleted file]
app/src/main/res/anim/slide_down.xml [deleted file]
app/src/main/res/anim/slide_in_right.xml [deleted file]
app/src/main/res/anim/slide_out_right.xml [deleted file]
app/src/main/res/anim/slide_up.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/app_icon.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/app_icon_dynamic.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/checkbox_star_black.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/checkbox_star_white.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/dashed_border_1dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/dashed_border_8dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/drop_shadow.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/fade_down_white.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_add_circle_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_add_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_assignment_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_cancel_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_check_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_clear_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_delete_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_event_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_event_gray_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_event_note_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_event_primary_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_exit_to_app_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_expand_less_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_expand_more_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_filter_list_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_filter_list_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_home_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_info_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_keyboard_arrow_down_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_launcher_background.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_menu_manage.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_menu_send.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_menu_share.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_mode_edit_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_more_horiz_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_notifications_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_palette_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_refresh_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_save_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_settings_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_star_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_star_border_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_star_border_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_star_white_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_sync_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_thick_check_white.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_unfold_more_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/ic_view_list_black_24dp.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/list_divider.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/list_divider_inside_out.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/side_nav_bar.xml [deleted file]
app/src/main/res/drawable-anydpi-v21/svg_thick_plus_white.xml [deleted file]
app/src/main/res/drawable-anydpi-v26/app_icon.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi-v26/app_icon_round.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/app_icon.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/dashed_border_8dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/drop_shadow.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/fade_down_white.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_add_circle_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_add_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_assignment_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_auto_graph_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_drag_handle_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_help_24_white.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_help_outline_24_primary.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_qr_code_scanner_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_clear_accent_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_comment_gray_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_delete_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_error_outline_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_event_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_event_gray_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_event_note_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_expand_less_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_filter_list_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_filter_list_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_home_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_mode_edit_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_palette_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_refresh_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_save_white_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/ic_settings_black_24dp.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/list_divider.xml [new file with mode: 0644]
app/src/main/res/drawable-anydpi/side_nav_bar.xml [new file with mode: 0644]
app/src/main/res/drawable-hdpi/app_icon.png [deleted file]
app/src/main/res/drawable-ldpi/app_icon.png [deleted file]
app/src/main/res/drawable-mdpi/app_icon.png [deleted file]
app/src/main/res/drawable-tvdpi/app_icon.png [deleted file]
app/src/main/res/drawable-xhdpi/app_icon.png [deleted file]
app/src/main/res/drawable-xxhdpi/app_icon.png [deleted file]
app/src/main/res/drawable-xxxhdpi/app_icon.png [deleted file]
app/src/main/res/drawable/app_icon_transparent_bg.xml [new file with mode: 0644]
app/src/main/res/drawable/launcher_foreground.xml [new file with mode: 0644]
app/src/main/res/drawable/thick_plus_icon.xml [new file with mode: 0644]
app/src/main/res/layout-w900dp/profile_list.xml [deleted file]
app/src/main/res/layout/account_autocomplete_row.xml [new file with mode: 0644]
app/src/main/res/layout/account_list_row.xml [new file with mode: 0644]
app/src/main/res/layout/account_list_summary_row.xml [new file with mode: 0644]
app/src/main/res/layout/account_summary_fragment.xml
app/src/main/res/layout/account_summary_row.xml [deleted file]
app/src/main/res/layout/activity_main.xml
app/src/main/res/layout/activity_new_transaction.xml
app/src/main/res/layout/activity_profile_detail.xml
app/src/main/res/layout/activity_templates.xml [new file with mode: 0644]
app/src/main/res/layout/date_picker_view.xml
app/src/main/res/layout/fragment_backups.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_currency_selector.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_currency_selector_list.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_new_transaction.xml
app/src/main/res/layout/fragment_new_transaction_saving.xml
app/src/main/res/layout/fragment_template_detail_source_selector.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_template_detail_source_selector_list.xml [new file with mode: 0644]
app/src/main/res/layout/fragment_template_list.xml [new file with mode: 0644]
app/src/main/res/layout/last_update_layout.xml [new file with mode: 0644]
app/src/main/res/layout/loading.xml [deleted file]
app/src/main/res/layout/main_navigation.xml [deleted file]
app/src/main/res/layout/nav_header_layout.xml [new file with mode: 0644]
app/src/main/res/layout/nav_header_logo.xml
app/src/main/res/layout/nav_profile_list_head.xml [deleted file]
app/src/main/res/layout/new_transaction_account_row.xml [new file with mode: 0644]
app/src/main/res/layout/new_transaction_header_row.xml [new file with mode: 0644]
app/src/main/res/layout/new_transaction_row.xml [deleted file]
app/src/main/res/layout/no_profiles.xml [deleted file]
app/src/main/res/layout/profile_detail.xml
app/src/main/res/layout/profile_list.xml [deleted file]
app/src/main/res/layout/profile_list_content.xml
app/src/main/res/layout/splash_activity_layout.xml [new file with mode: 0644]
app/src/main/res/layout/switch_item.xml
app/src/main/res/layout/template_details_account.xml [new file with mode: 0644]
app/src/main/res/layout/template_details_fragment.xml [new file with mode: 0644]
app/src/main/res/layout/template_details_header.xml [new file with mode: 0644]
app/src/main/res/layout/template_list_template_item.xml [new file with mode: 0644]
app/src/main/res/layout/templates_fallback_divider.xml [new file with mode: 0644]
app/src/main/res/layout/transaction_delimiter.xml [new file with mode: 0644]
app/src/main/res/layout/transaction_list_fragment.xml
app/src/main/res/layout/transaction_list_row.xml
app/src/main/res/layout/transaction_list_row_accounts_table_row.xml [new file with mode: 0644]
app/src/main/res/menu/account_list.xml [new file with mode: 0644]
app/src/main/res/menu/account_summary.xml [deleted file]
app/src/main/res/menu/api_version.xml [new file with mode: 0644]
app/src/main/res/menu/future_dates.xml
app/src/main/res/menu/new_transaction.xml
app/src/main/res/menu/new_transaction_fragment.xml
app/src/main/res/menu/profile_list.xml [deleted file]
app/src/main/res/menu/template_details_menu.xml [new file with mode: 0644]
app/src/main/res/menu/template_list_menu.xml [new file with mode: 0644]
app/src/main/res/menu/transaction_list.xml
app/src/main/res/mipmap-hdpi/ic_launcher.png [deleted file]
app/src/main/res/mipmap-hdpi/ic_launcher_round.png [deleted file]
app/src/main/res/mipmap-mdpi/ic_launcher.png [deleted file]
app/src/main/res/mipmap-mdpi/ic_launcher_round.png [deleted file]
app/src/main/res/mipmap-xhdpi/ic_launcher.png [deleted file]
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png [deleted file]
app/src/main/res/mipmap-xxhdpi/ic_launcher.png [deleted file]
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png [deleted file]
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png [deleted file]
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png [deleted file]
app/src/main/res/navigation/new_transaction_navigation.xml
app/src/main/res/navigation/template_list_navigation.xml [new file with mode: 0644]
app/src/main/res/raw/create_db.sql [deleted file]
app/src/main/res/raw/db_17.sql [new file with mode: 0644]
app/src/main/res/raw/db_18.sql [new file with mode: 0644]
app/src/main/res/raw/db_19.sql [new file with mode: 0644]
app/src/main/res/raw/db_20.sql [new file with mode: 0644]
app/src/main/res/raw/db_20_22.sql [new file with mode: 0644]
app/src/main/res/raw/db_22_30.sql [new file with mode: 0644]
app/src/main/res/raw/db_30_32.sql [new file with mode: 0644]
app/src/main/res/raw/db_32_34.sql [new file with mode: 0644]
app/src/main/res/raw/db_34_40.sql [new file with mode: 0644]
app/src/main/res/raw/db_41.sql [new file with mode: 0644]
app/src/main/res/raw/db_41_58.sql [new file with mode: 0644]
app/src/main/res/raw/db_59.sql [new file with mode: 0644]
app/src/main/res/raw/db_60.sql [new file with mode: 0644]
app/src/main/res/raw/db_61.sql [new file with mode: 0644]
app/src/main/res/raw/db_62.sql [new file with mode: 0644]
app/src/main/res/raw/db_63.sql [new file with mode: 0644]
app/src/main/res/raw/db_64.sql [new file with mode: 0644]
app/src/main/res/raw/sql_0.sql [deleted file]
app/src/main/res/raw/sql_1.sql [deleted file]
app/src/main/res/raw/sql_10.sql [deleted file]
app/src/main/res/raw/sql_11.sql [deleted file]
app/src/main/res/raw/sql_12.sql [deleted file]
app/src/main/res/raw/sql_13.sql [deleted file]
app/src/main/res/raw/sql_14.sql [deleted file]
app/src/main/res/raw/sql_15.sql [deleted file]
app/src/main/res/raw/sql_16.sql [deleted file]
app/src/main/res/raw/sql_17.sql [deleted file]
app/src/main/res/raw/sql_18.sql [deleted file]
app/src/main/res/raw/sql_19.sql [deleted file]
app/src/main/res/raw/sql_2.sql [deleted file]
app/src/main/res/raw/sql_20.sql [deleted file]
app/src/main/res/raw/sql_21.sql [deleted file]
app/src/main/res/raw/sql_22.sql [deleted file]
app/src/main/res/raw/sql_23.sql [deleted file]
app/src/main/res/raw/sql_24.sql [deleted file]
app/src/main/res/raw/sql_3.sql [deleted file]
app/src/main/res/raw/sql_4.sql [deleted file]
app/src/main/res/raw/sql_5.sql [deleted file]
app/src/main/res/raw/sql_6.sql [deleted file]
app/src/main/res/raw/sql_7.sql [deleted file]
app/src/main/res/raw/sql_8.sql [deleted file]
app/src/main/res/raw/sql_9.sql [deleted file]
app/src/main/res/values-bg/arrays.xml
app/src/main/res/values-bg/strings.xml
app/src/main/res/values-h360dp/dimens.xml
app/src/main/res/values-night/styles.xml [new file with mode: 0644]
app/src/main/res/values-v26/styles.xml
app/src/main/res/values/arrays.xml
app/src/main/res/values/attr.xml
app/src/main/res/values/colors.xml
app/src/main/res/values/dimens.xml
app/src/main/res/values/ic_launcher_background.xml [new file with mode: 0644]
app/src/main/res/values/ids.xml
app/src/main/res/values/strings.xml
app/src/main/res/values/styles.xml
app/src/main/res/xml/backup_descriptor.xml [deleted file]
app/src/main/res/xml/network_security_config.xml
app/src/main/res/xml/pref_data_sync.xml [deleted file]
app/src/main/res/xml/pref_headers.xml [deleted file]
app/src/main/res/xml/pref_interface.xml [deleted file]
app/src/main/res/xml/pref_notification.xml [deleted file]
app/src/test/java/net/ktnx/mobileledger/async/LegacyParserTest.java [new file with mode: 0644]
app/src/test/java/net/ktnx/mobileledger/model/LedgerAccountTest.java [new file with mode: 0644]
app/src/test/java/net/ktnx/mobileledger/utils/SimpleDateTest.java [new file with mode: 0644]
art/app-icon-transparent-bg.svg [new file with mode: 0644]
art/app-icon.svg
art/thick-plus-icon.svg [new file with mode: 0644]
build.gradle
gradle.properties
gradle/wrapper/gradle-wrapper.properties
gradlew.bat
metadata/bg-BG/changelogs/30.txt
metadata/bg-BG/changelogs/31.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/32.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/33.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/34.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/35.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/36.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/37.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/38.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/39.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/40.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/41.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/42.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/43.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/44.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/45.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/46.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/47.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/48.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/49.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/50.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/51.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/52.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/53.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/54.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/55.txt [new file with mode: 0644]
metadata/bg-BG/changelogs/56.txt [new file with mode: 0644]
metadata/bg-BG/full_description.txt
metadata/bg-BG/images/icon.png [new file with mode: 0644]
metadata/bg-BG/images/phoneScreenshots/drawer-open.png
metadata/en-US/changelogs/20.txt
metadata/en-US/changelogs/30.txt
metadata/en-US/changelogs/31.txt [new file with mode: 0644]
metadata/en-US/changelogs/32.txt [new file with mode: 0644]
metadata/en-US/changelogs/33.txt [new file with mode: 0644]
metadata/en-US/changelogs/34.txt [new file with mode: 0644]
metadata/en-US/changelogs/35.txt [new file with mode: 0644]
metadata/en-US/changelogs/36.txt [new file with mode: 0644]
metadata/en-US/changelogs/37.txt [new file with mode: 0644]
metadata/en-US/changelogs/38.txt [new file with mode: 0644]
metadata/en-US/changelogs/39.txt [new file with mode: 0644]
metadata/en-US/changelogs/40.txt [new file with mode: 0644]
metadata/en-US/changelogs/41.txt [new file with mode: 0644]
metadata/en-US/changelogs/42.txt [new file with mode: 0644]
metadata/en-US/changelogs/43.txt [new file with mode: 0644]
metadata/en-US/changelogs/44.txt [new file with mode: 0644]
metadata/en-US/changelogs/45.txt [new file with mode: 0644]
metadata/en-US/changelogs/46.txt [new file with mode: 0644]
metadata/en-US/changelogs/47.txt [new file with mode: 0644]
metadata/en-US/changelogs/48.txt [new file with mode: 0644]
metadata/en-US/changelogs/49.txt [new file with mode: 0644]
metadata/en-US/changelogs/50.txt [new file with mode: 0644]
metadata/en-US/changelogs/51.txt [new file with mode: 0644]
metadata/en-US/changelogs/52.txt [new file with mode: 0644]
metadata/en-US/changelogs/53.txt [new file with mode: 0644]
metadata/en-US/changelogs/54.txt [new file with mode: 0644]
metadata/en-US/changelogs/55.txt [new file with mode: 0644]
metadata/en-US/changelogs/56.txt [new file with mode: 0644]
metadata/en-US/full_description.txt
metadata/en-US/images/icon.png [new file with mode: 0644]
metadata/en-US/images/phoneScreenshots/drawer-open.png
tools/gen-styles
tools/populate-app-icon

index 363190b34a0200173d3ab4d989dcfe116267e91f..02f84284cacf6943424f157b78ec8e114eb5dd6f 100644 (file)
@@ -1,5 +1,231 @@
 # Changes
 
 # 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
+    + splash screen on startup
+    + show account/transaction counts
+* IMPROVEMENTS
+    + theme fixes, improved contrast
+    + better responsiveness, more work moved to background threads
+    + faster storage of retrieved data
+    + last update info moved to lists to save space
+* FIXES
+    + fixed progress of data retrieval from hledger-web
+    + fixed extra fetches of remote data
+    + fill currency list with data from the journal
+
+## [0.14.1] - 2020-06-28
+
+* IMPROVEMENTS
+    + better theme support, especially in system-wide dark mode
+* FIXES
+    + restore f-droid listing icon
+
+## [0.14.0] - 2020-06-18
+
+* NEW
+    + show transaction-level comment in transaction list
+    + scroll to a specific date in the transaction list
+* IMPROVEMENTS
+    + better all-around theming; employ some material design recommendations
+    + follow system-wide font size settings
+* FIXES
+    + fix a crash upon profile theme change
+    + fix a crash when returning to the new transaction entry with the date
+      picker open
+    + various small fixes
+
+## [0.13.1] - 2020-05-15
+
+* additional, universal fix for entering numbers
+
+## [0.13.0] - 2020-05-14
+
+* NEW
+    + transaction-level comment entry
+    + ability to hide comment entry, per profile
+* FIXES:
+    + fixed crash when parsing posting flags with hledger-web before 1.14
+    + visual fixes
+    + fix numerical entry with some samsung keyboards
+
+## [0.12.0] - 2020-05-06
+
+* NEW
+    + support for adding account-level comments for new transactions
+    + currency/commodity support in new transaction screen, per-profile default commodity
+    + control of entry dates in the future
+    + support 1.14 and 1.15+ JSON API
+* IMPROVEMENTS
+    + darker yellow, green and cyan theme colours
+    + Profiles:
+        - suggest distinct color for new profiles
+        - improved profile editor interface
+    + avoid UI lockup while looking for a previous transaction with the chosen description
+* FIXES
+    + restore ability to scroll the profile details screen
+    + remove profile-specific options from the database when removing a profile
+    + consistent item colors in the profile details
+    + fixed stuck refreshing indicator when main view is slid to the transaction list while transactions are loading
+    + limit the number of launcher shortcuts to the maximum supported
+
 ## [0.11.0] - 2019-12-01
 
 * NEW
 ## [0.11.0] - 2019-12-01
 
 * NEW
index a2032f1de32a1df01fcb5fa3dc988be44dfb50ad..a9db50f35f6e0771bc62a8388cf1a80513945390 100644 (file)
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ MoLe is a front-end to [[https://hledger.org/hledger-web.html hledger-web]]
 
 ## Main software
 
 
 ## Main software
 
-Copyright ⓒ 2018, 2019 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
 
 MoLe is free software: you can distribute it and/or modify it
 under the term of the GNU General Public License as published by
@@ -17,7 +17,8 @@ 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
 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
 
 
 ## Libraries
 
@@ -34,10 +35,12 @@ Apache (Software) License, version 2.0 ("the License").
 See the License for details about distribution rights, and the
 specific rights regarding derivate works.
 
 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
 
 
 ## 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>)
diff --git a/TODO.txt b/TODO.txt
new file mode 100644 (file)
index 0000000..e229e17
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,7 @@
+* 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
index 119bf6f096b489294b63ff1d76e994dff014279f..71673f6eb8dca03508bc0f12f40580f9893fda37 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright © 2019 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
  * This file is part of MoLe.
  * 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 {
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 28
+    compileSdkVersion 31
     defaultConfig {
         applicationId "net.ktnx.mobileledger"
         minSdkVersion 22
     defaultConfig {
         applicationId "net.ktnx.mobileledger"
         minSdkVersion 22
-        targetSdkVersion 28
-        versionCode 31
-        versionName '0.12.0'
+        targetSdkVersion 31
+        vectorDrawables.useSupportLibrary true
+        versionCode 56
+        versionName '0.21.7'
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        javaCompileOptions {
+            annotationProcessorOptions {
+                arguments += [
+                        "room.schemaLocation"  : "$projectDir/schemas".toString(),
+                        "room.incremental"     : "true",
+                        "room.expandProjection": "true"
+                ]
+            }
+        }
     }
     buildTypes {
         release {
     }
     buildTypes {
         release {
@@ -36,31 +46,54 @@ android {
             versionNameSuffix '-debug'
             applicationIdSuffix '.debug'
         }
             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/'] } }
     }
     sourceSets { main { assets.srcDirs = ['src/main/assets', 'src/main/assets/'] } }
-    buildToolsVersion '28.0.3'
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
     productFlavors {
     }
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
     productFlavors {
     }
+    buildFeatures.viewBinding = true
+    buildToolsVersion '30.0.3'
+    namespace 'net.ktnx.mobileledger'
 }
 
 dependencies {
 }
 
 dependencies {
-    def nav_version = '2.2.0-rc04'
+    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 fileTree(include: ['*.jar'], dir: 'libs')
-    implementation 'androidx.appcompat:appcompat:1.1.0'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation 'com.google.android.material:material:1.2.0-alpha02'
-    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
-    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-rc03'
-    implementation 'androidx.recyclerview:recyclerview:1.1.0'
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test:runner:1.3.0-alpha03'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0-alpha03'
-    implementation 'org.jetbrains:annotations:15.0'
-    implementation 'com.fasterxml.jackson.module:jackson-modules-java8:2.10.1'
+    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.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.navigation:navigation-fragment:$nav_version"
     implementation "androidx.navigation:navigation-ui:$nav_version"
+    implementation 'androidx.appcompat:appcompat:1.6.0-alpha01'
+}
+
+allprojects {
+    gradle.projectsEvaluated {
+        tasks.withType(JavaCompile) {
+            options.compilerArgs <<
+                    "-Xlint:deprecation" <<
+                    "-Xlint:unchecked"
+        }
+    }
 }
 }
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/1.json b/app/schemas/net.ktnx.mobileledger.db.DB/1.json
new file mode 100644 (file)
index 0000000..5913a85
--- /dev/null
@@ -0,0 +1,192 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/43.json b/app/schemas/net.ktnx.mobileledger.db.DB/43.json
new file mode 100644 (file)
index 0000000..d98ded0
--- /dev/null
@@ -0,0 +1,192 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/44.json b/app/schemas/net.ktnx.mobileledger.db.DB/44.json
new file mode 100644 (file)
index 0000000..8b602ce
--- /dev/null
@@ -0,0 +1,192 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/45.json b/app/schemas/net.ktnx.mobileledger.db.DB/45.json
new file mode 100644 (file)
index 0000000..0b47e0b
--- /dev/null
@@ -0,0 +1,192 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/46.json b/app/schemas/net.ktnx.mobileledger.db.DB/46.json
new file mode 100644 (file)
index 0000000..e4b78ce
--- /dev/null
@@ -0,0 +1,192 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/47.json b/app/schemas/net.ktnx.mobileledger.db.DB/47.json
new file mode 100644 (file)
index 0000000..96a61f7
--- /dev/null
@@ -0,0 +1,271 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/48.json b/app/schemas/net.ktnx.mobileledger.db.DB/48.json
new file mode 100644 (file)
index 0000000..72939fa
--- /dev/null
@@ -0,0 +1,271 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/49.json b/app/schemas/net.ktnx.mobileledger.db.DB/49.json
new file mode 100644 (file)
index 0000000..09dc625
--- /dev/null
@@ -0,0 +1,271 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/50.json b/app/schemas/net.ktnx.mobileledger.db.DB/50.json
new file mode 100644 (file)
index 0000000..c8f081e
--- /dev/null
@@ -0,0 +1,277 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/51.json b/app/schemas/net.ktnx.mobileledger.db.DB/51.json
new file mode 100644 (file)
index 0000000..f755a4b
--- /dev/null
@@ -0,0 +1,287 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/52.json b/app/schemas/net.ktnx.mobileledger.db.DB/52.json
new file mode 100644 (file)
index 0000000..883df1f
--- /dev/null
@@ -0,0 +1,287 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/53.json b/app/schemas/net.ktnx.mobileledger.db.DB/53.json
new file mode 100644 (file)
index 0000000..559d1ad
--- /dev/null
@@ -0,0 +1,293 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/54.json b/app/schemas/net.ktnx.mobileledger.db.DB/54.json
new file mode 100644 (file)
index 0000000..fcaec8c
--- /dev/null
@@ -0,0 +1,276 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/55.json b/app/schemas/net.ktnx.mobileledger.db.DB/55.json
new file mode 100644 (file)
index 0000000..8c790a3
--- /dev/null
@@ -0,0 +1,276 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/56.json b/app/schemas/net.ktnx.mobileledger.db.DB/56.json
new file mode 100644 (file)
index 0000000..f60708c
--- /dev/null
@@ -0,0 +1,342 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/57.json b/app/schemas/net.ktnx.mobileledger.db.DB/57.json
new file mode 100644 (file)
index 0000000..68311f5
--- /dev/null
@@ -0,0 +1,348 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/58.json b/app/schemas/net.ktnx.mobileledger.db.DB/58.json
new file mode 100644 (file)
index 0000000..c4dea2e
--- /dev/null
@@ -0,0 +1,776 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/59.json b/app/schemas/net.ktnx.mobileledger.db.DB/59.json
new file mode 100644 (file)
index 0000000..5896bca
--- /dev/null
@@ -0,0 +1,861 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/60.json b/app/schemas/net.ktnx.mobileledger.db.DB/60.json
new file mode 100644 (file)
index 0000000..f65c93d
--- /dev/null
@@ -0,0 +1,828 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/61.json b/app/schemas/net.ktnx.mobileledger.db.DB/61.json
new file mode 100644 (file)
index 0000000..3a92716
--- /dev/null
@@ -0,0 +1,834 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/62.json b/app/schemas/net.ktnx.mobileledger.db.DB/62.json
new file mode 100644 (file)
index 0000000..eb64dfb
--- /dev/null
@@ -0,0 +1,843 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/63.json b/app/schemas/net.ktnx.mobileledger.db.DB/63.json
new file mode 100644 (file)
index 0000000..f672f70
--- /dev/null
@@ -0,0 +1,867 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/64.json b/app/schemas/net.ktnx.mobileledger.db.DB/64.json
new file mode 100644 (file)
index 0000000..2155828
--- /dev/null
@@ -0,0 +1,867 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/65.json b/app/schemas/net.ktnx.mobileledger.db.DB/65.json
new file mode 100644 (file)
index 0000000..9b4fd86
--- /dev/null
@@ -0,0 +1,867 @@
+{
+  "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
diff --git a/app/schemas/net.ktnx.mobileledger.db.DB/66.json b/app/schemas/net.ktnx.mobileledger.db.DB/66.json
new file mode 100644 (file)
index 0000000..13cd5d8
--- /dev/null
@@ -0,0 +1,867 @@
+{
+  "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
index 51950343ea6dae5b2c35c612a90cdea216a2f702..b17a1ecfcacfaec410eee37f6be145c34c967ac7 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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"
   ~ 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" />
 
     <application
         android:name=".App"
         android:allowBackup="true"
 
     <uses-permission android:name="android.permission.INTERNET" />
 
     <application
         android:name=".App"
         android:allowBackup="true"
-        android:fullBackupContent="@xml/backup_descriptor"
+        android:appCategory="productivity"
         android:icon="@drawable/app_icon"
         android:label="@string/app_name"
         android:icon="@drawable/app_icon"
         android:label="@string/app_name"
-        android:roundIcon="@drawable/app_icon"
-        android:supportsRtl="true"
         android:networkSecurityConfig="@xml/network_security_config"
         android:networkSecurityConfig="@xml/network_security_config"
-        android:theme="@style/AppTheme"
+        android:roundIcon="@drawable/app_icon_round"
+        android:supportsRtl="true"
+        android:backupAgent=".backup.MobileLedgerBackupAgent"
         tools:ignore="GoogleAppIndexingWarning">
         <activity
         tools:ignore="GoogleAppIndexingWarning">
         <activity
-            android:name=".ui.activity.MainActivity"
-            android:label="@string/app_name"
-            android:theme="@style/AppTheme.NoActionBar">
+            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:exported="true"
+            android:theme="@style/AppTheme.default">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
         <activity
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
         <activity
-            android:name=".ui.activity.SettingsActivity"
-            android:label="@string/title_activity_settings"
-            android:parentActivityName=".ui.activity.MainActivity">
-            <meta-data
-                android:name="android.support.PARENT_ACTIVITY"
-                android:value="net.ktnx.mobileledger.ui.activity.MainActivity" />
-        </activity>
+            android:name=".ui.activity.MainActivity"
+            android:theme="@style/AppTheme.default" />
         <activity
         <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:label="@string/title_activity_new_transaction"
             android:parentActivityName=".ui.activity.MainActivity"
-            android:theme="@style/AppTheme.NoActionBar">
-            <meta-data
-                android:name="android.support.PARENT_ACTIVITY"
-                android:value=".ui.activity.MainActivity" />
-        </activity>
+            android:theme="@style/AppTheme.default"
+            android:windowSoftInputMode="stateVisible|adjustResize" />
         <activity
         <activity
-            android:name=".ui.activity.ProfileDetailActivity"
+            android:name=".ui.profiles.ProfileDetailActivity"
             android:label="@string/title_profile_details"
             android:parentActivityName=".ui.activity.MainActivity"
             android:label="@string/title_profile_details"
             android:parentActivityName=".ui.activity.MainActivity"
-            android:theme="@style/AppTheme.NoActionBar">
-            <meta-data
-                android:name="android.support.PARENT_ACTIVITY"
-                android:value=".ui.activity.MainActivity" />
-        </activity>
+            android:windowSoftInputMode="stateVisible|adjustResize" />
     </application>
 
 </manifest>
\ No newline at end of file
     </application>
 
 </manifest>
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644 (file)
index 0000000..bfa696b
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
index 920187e9fb54d98b69e0766ff097031ae0b53c7a..a9a416e7bb3c15a8e49c51f1aedb45f1307d0bee 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -21,58 +21,98 @@ import android.app.Application;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
-import android.database.sqlite.SQLiteDatabase;
-import android.preference.PreferenceManager;
 import android.util.Log;
 
 import net.ktnx.mobileledger.model.Data;
 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.Globals;
 import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MobileLedgerDatabase;
+
+import org.jetbrains.annotations.NotNull;
 
 import java.net.Authenticator;
 import java.net.MalformedURLException;
 import java.net.PasswordAuthentication;
 import java.net.URL;
 
 import java.net.Authenticator;
 import java.net.MalformedURLException;
 import java.net.PasswordAuthentication;
 import java.net.URL;
-
-import static net.ktnx.mobileledger.ui.activity.SettingsActivity.PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS;
+import java.util.Locale;
 
 public class App extends Application {
 
 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;
     public static App instance;
-    private MobileLedgerDatabase dbHelper;
-    public static SQLiteDatabase getDatabase() {
-        if (instance == null) throw new RuntimeException("Application not created yet");
-
-        return instance.getDB();
+    private static ProfileDetailModel profileModel;
+    private boolean monthNamesPrepared = false;
+    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()");
         instance = this;
         super.onCreate();
     }
     @Override
     public void onCreate() {
         Logger.debug("flow", "App onCreate()");
         instance = this;
         super.onCreate();
-        updateMonthNames();
-        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(this);
-        Data.optShowOnlyStarred.set(p.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
-        SharedPreferences.OnSharedPreferenceChangeListener handler =
-                (preference, value) -> Data.optShowOnlyStarred
-                        .set(preference.getBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, false));
-        p.registerOnSharedPreferenceChangeListener(handler);
+        Data.refreshCurrencyData(Locale.getDefault());
         Authenticator.setDefault(new Authenticator() {
             @Override
             protected PasswordAuthentication getPasswordAuthentication() {
         Authenticator.setDefault(new Authenticator() {
             @Override
             protected PasswordAuthentication getPasswordAuthentication() {
-                MobileLedgerProfile p = Data.profile.getValue();
-                if ((p != null) && p.isAuthEnabled()) {
+                if (getAuthEnabled()) {
                     try {
                     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))
                         final String requestingHost = getRequestingHost();
                         final String expectedHost = url.getHost();
                         if (requestingHost.equalsIgnoreCase(expectedHost))
-                            return new PasswordAuthentication(p.getAuthUserName(),
-                                    p.getAuthPassword().toCharArray());
-                        else Log.w("http-auth",
-                                String.format("Requesting host [%s] differs from expected [%s]",
-                                        requestingHost, expectedHost));
+                            return new PasswordAuthentication(getAuthUserName(),
+                                    getAuthPassword().toCharArray());
+                        else
+                            Log.w("http-auth",
+                                    String.format("Requesting host [%s] differs from expected [%s]",
+                                            requestingHost, expectedHost));
                     }
                     catch (MalformedURLException e) {
                         e.printStackTrace();
                     }
                     catch (MalformedURLException e) {
                         e.printStackTrace();
@@ -83,32 +123,18 @@ public class App extends Application {
             }
         });
     }
             }
         });
     }
-    private void updateMonthNames() {
+    private void prepareMonthNames(boolean force) {
+        if (force || monthNamesPrepared)
+            return;
         Resources rm = getResources();
         Globals.monthNames = rm.getStringArray(R.array.month_names);
         Resources rm = getResources();
         Globals.monthNames = rm.getStringArray(R.array.month_names);
+        monthNamesPrepared = true;
     }
     @Override
     }
     @Override
-    public void onTerminate() {
-        Logger.debug("flow", "App onTerminate()");
-        if (dbHelper != null) dbHelper.close();
-        super.onTerminate();
-    }
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
+    public void onConfigurationChanged(@NotNull Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         super.onConfigurationChanged(newConfig);
-        updateMonthNames();
-    }
-    public SQLiteDatabase getDB() {
-        if (dbHelper == null) initDb();
-
-        final SQLiteDatabase db = dbHelper.getWritableDatabase();
-        db.execSQL("pragma case_sensitive_like=ON;");
-
-        return db;
-    }
-    private synchronized void initDb() {
-        if (dbHelper != null) return;
-
-        dbHelper = new MobileLedgerDatabase(this);
+        prepareMonthNames(true);
+        Data.refreshCurrencyData(Locale.getDefault());
+        Data.locale.setValue(Locale.getDefault());
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java b/app/src/main/java/net/ktnx/mobileledger/BackupsActivity.java
new file mode 100644 (file)
index 0000000..a2b5cf6
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTask.java
deleted file mode 100644 (file)
index bde0156..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.async;
-
-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.LedgerAccount;
-import net.ktnx.mobileledger.utils.LockHolder;
-
-import java.util.ArrayList;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class CommitAccountsTask
-        extends AsyncTask<CommitAccountsTaskParams, Void, ArrayList<LedgerAccount>> {
-    protected ArrayList<LedgerAccount> doInBackground(CommitAccountsTaskParams... params) {
-        Data.backgroundTaskStarted();
-        ArrayList<LedgerAccount> newList = new ArrayList<>();
-        String profile = Data.profile.getValue().getUuid();
-        try {
-
-            SQLiteDatabase db = App.getDatabase();
-            db.beginTransaction();
-            try {
-                try (LockHolder lh = params[0].accountList.lockForWriting()) {
-                    for (int i = 0; i < params[0].accountList.size(); i++ ){
-                        LedgerAccount acc = params[0].accountList.get(i);
-                        debug("CAT", String.format("Setting %s to %s", acc.getName(),
-                                acc.isHiddenByStarToBe() ? "hidden" : "starred"));
-                        db.execSQL("UPDATE accounts SET hidden=? WHERE profile=? AND name=?",
-                                new Object[]{acc.isHiddenByStarToBe() ? 1 : 0, profile, acc.getName()
-                                });
-
-                        acc.setHiddenByStar(acc.isHiddenByStarToBe());
-                        if (!params[0].showOnlyStarred || !acc.isHiddenByStar()) newList.add(acc);
-                    }
-                    db.setTransactionSuccessful();
-                }
-            }
-            finally {
-                db.endTransaction();
-            }
-        }
-        finally {
-            Data.backgroundTaskFinished();
-        }
-
-        return newList;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTaskParams.java b/app/src/main/java/net/ktnx/mobileledger/async/CommitAccountsTaskParams.java
deleted file mode 100644 (file)
index ae3a0b9..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.async;
-
-import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.utils.ObservableList;
-
-public class CommitAccountsTaskParams {
-    ObservableList<LedgerAccount> accountList;
-    boolean showOnlyStarred;
-    public CommitAccountsTaskParams(ObservableList<LedgerAccount> accountList, boolean showOnlyStarred) {
-        this.accountList = accountList;
-        this.showOnlyStarred = showOnlyStarred;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/DbOpItem.java b/app/src/main/java/net/ktnx/mobileledger/async/DbOpItem.java
deleted file mode 100644 (file)
index 7ecce27..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.async;
-
-class DbOpItem {
-    String sql;
-    Object[] params;
-    public DbOpItem(String sql, Object[] params) {
-        this.sql = sql;
-        this.params = params;
-    }
-    public DbOpItem(String sql) {
-        this(sql, null);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/DbOpQueue.java b/app/src/main/java/net/ktnx/mobileledger/async/DbOpQueue.java
deleted file mode 100644 (file)
index 2bc541e..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.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) {
-        init();
-        debug("opQueue", "Adding " + sql);
-        queue.add(new DbOpItem(sql, params));
-    }
-    static void add(String sql) {
-        queue.add(new DbOpItem(sql));
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/DbOpRunner.java b/app/src/main/java/net/ktnx/mobileledger/async/DbOpRunner.java
deleted file mode 100644 (file)
index 3a81614..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.async;
-
-import android.database.sqlite.SQLiteDatabase;
-
-import net.ktnx.mobileledger.App;
-
-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();
-                debug("opQrunner", "Executing "+item.sql);
-                db.execSQL(item.sql, item.params);
-            }
-            catch (InterruptedException e) {
-                break;
-            }
-        }
-    }
-}
index 992abd1fafd7263fb477a21620f220fa627c2dbf..20d669413f12c6e6d92014cdecfcf78e6b3cf703 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -18,5 +18,5 @@
 package net.ktnx.mobileledger.async;
 
 public interface DescriptionSelectedCallback {
 package net.ktnx.mobileledger.async;
 
 public interface DescriptionSelectedCallback {
-    void descriptionSelected(String description);
+    void onDescriptionSelected(String description);
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/GeneralBackgroundTasks.java b/app/src/main/java/net/ktnx/mobileledger/async/GeneralBackgroundTasks.java
new file mode 100644 (file)
index 0000000..3ff5904
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/RefreshDescriptionsTask.java
deleted file mode 100644 (file)
index 62885c1..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.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 java.util.HashMap;
-import java.util.Map;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class RefreshDescriptionsTask extends AsyncTask<Void, Void, Void> {
-    @Override
-    protected Void doInBackground(Void... voids) {
-        Map<String, Boolean> unique = new HashMap<>();
-
-        debug("descriptions", "Starting refresh");
-        SQLiteDatabase db = App.getDatabase();
-
-        Data.backgroundTaskStarted();
-        try {
-            db.beginTransaction();
-            try {
-                db.execSQL("UPDATE description_history set keep=0");
-                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;
-
-                        db.execSQL(
-                                "replace into description_history(description, description_upper, " +
-                                "keep) values(?, ?, 1)", new String[]{description, descriptionUpper});
-                        unique.put(descriptionUpper, true);
-                    }
-                }
-                db.execSQL("DELETE from description_history where keep=0");
-                db.setTransactionSuccessful();
-                debug("descriptions", "Refresh successful");
-            }
-            finally {
-                db.endTransaction();
-            }
-        }
-        finally {
-            Data.backgroundTaskFinished();
-            debug("descriptions", "Refresh done");
-        }
-
-        return null;
-    }
-}
index 5aebe219ffa6cb9650ce15afd30719b6ce2ae150..0b751b4c6d7df0c94174973ec7a2e449352c9dc9 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 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 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.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.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.activity.MainActivity;
+import net.ktnx.mobileledger.utils.Logger;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.lang.ref.WeakReference;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
-import java.util.Stack;
+import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static net.ktnx.mobileledger.utils.Logger.debug;
 
 
-
-public class RetrieveTransactionsTask
-        extends AsyncTask<Void, RetrieveTransactionsTask.Progress, String> {
+public class RetrieveTransactionsTask extends Thread {
     private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
     private static final Pattern reComment = Pattern.compile("^\\s*;");
     private static final int MATCHING_TRANSACTIONS_LIMIT = 150;
     private static final Pattern reComment = Pattern.compile("^\\s*;");
-    private static final Pattern reTransactionStart = Pattern.compile("<tr class=\"title\" " +
-                                                                      "id=\"transaction-(\\d+)\"><td class=\"date\"[^\"]*>([\\d.-]+)</td>");
+    private static final Pattern reTransactionStart = Pattern.compile(
+            "<tr class=\"title\" " + "id=\"transaction-(\\d+)" + "\"><td class=\"date" +
+            "\"[^\"]*>([\\d.-]+)</td>");
     private static final Pattern reTransactionDescription =
             Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
     private static final Pattern reTransactionDescription =
             Pattern.compile("<tr class=\"posting\" title=\"(\\S+)\\s(.+)");
-    private static final Pattern reTransactionDetails =
-            Pattern.compile("^\\s+(\\S[\\S\\s]+\\S)\\s\\s+([-+]?\\d[\\d,.]*)(?:\\s+(\\S+)$)?");
+    private static final Pattern reTransactionDetails = Pattern.compile(
+            "^\\s+" + "([!*]\\s+)?" + "(\\S[\\S\\s]+\\S)\\s\\s+" + "(?:([^\\d\\s+\\-]+)\\s*)?" +
+            "([-+]?\\d[\\d,.]*)" + "(?:\\s*([^\\d\\s+\\-]+)\\s*$)?");
     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 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 WeakReference<MainActivity> contextRef;
+    private static final String TAG = "RTT";
     // %3A is '='
     // %3A is '='
-    private Pattern reAccountName = Pattern.compile("/register\\?q=inacct%3A([a-zA-Z0-9%]+)\"");
-    private Pattern reAccountValue = Pattern.compile(
+    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>");
             "<span class=\"[^\"]*\\bamount\\b[^\"]*\">\\s*([-+]?[\\d.,]+)(?:\\s+(\\S+))?</span>");
-    private MobileLedgerProfile profile;
-    public RetrieveTransactionsTask(WeakReference<MainActivity> contextRef,
-                                    @NonNull MobileLedgerProfile profile) {
-        this.contextRef = contextRef;
+    private final Profile profile;
+    private int expectedPostingsCount = -1;
+    public RetrieveTransactionsTask(@NonNull Profile profile) {
         this.profile = profile;
     }
     private static void L(String msg) {
         //debug("transaction-parser", msg);
     }
         this.profile = profile;
     }
     private static void L(String msg) {
         //debug("transaction-parser", msg);
     }
-    @Override
-    protected void onProgressUpdate(Progress... values) {
-        super.onProgressUpdate(values);
-        MainActivity context = getContext();
-        if (context == null) return;
-        context.onRetrieveProgress(values[0]);
+    static LedgerTransactionAccount parseTransactionAccountLine(String line) {
+        Matcher m = reTransactionDetails.matcher(line);
+        if (m.find()) {
+            String postingStatus = m.group(1);
+            String acc_name = m.group(2);
+            String currencyPre = m.group(3);
+            String amount = Objects.requireNonNull(m.group(4));
+            String currencyPost = m.group(5);
+
+            String currency = null;
+            if ((currencyPre != null) && (currencyPre.length() > 0)) {
+                if ((currencyPost != null) && (currencyPost.length() > 0))
+                    return null;
+                currency = currencyPre;
+            }
+            else if ((currencyPost != null) && (currencyPost.length() > 0)) {
+                currency = currencyPost;
+            }
+
+            amount = amount.replace(',', '.');
+
+            return new LedgerTransactionAccount(acc_name, Float.parseFloat(amount), currency, null);
+        }
+        else {
+            return null;
+        }
     }
     }
-    @Override
-    protected void onPreExecute() {
-        super.onPreExecute();
-        MainActivity context = getContext();
-        if (context == null) return;
-        context.onRetrieveStart();
+    private void publishProgress(Progress progress) {
+        Data.backgroundTaskProgress.postValue(progress);
     }
     }
-    @Override
-    protected void onPostExecute(String error) {
-        super.onPostExecute(error);
-        MainActivity context = getContext();
-        if (context == null) return;
-        context.onRetrieveDone(error);
-    }
-    @Override
-    protected void onCancelled() {
-        super.onCancelled();
-        MainActivity context = getContext();
-        if (context == null) return;
-        context.onRetrieveDone(null);
+    private void finish(Result result) {
+        Progress progress = new Progress();
+        progress.setState(ProgressState.FINISHED);
+        progress.setError(result.error);
+        publishProgress(progress);
     }
     }
-    private String retrieveTransactionListLegacy()
-            throws IOException, ParseException, HTTPException {
+    private void cancel() {
         Progress progress = new Progress();
         Progress progress = new Progress();
-        int maxTransactionId = Progress.INDETERMINATE;
-        ArrayList<LedgerAccount> accountList = new ArrayList<>();
-        HashMap<String, Void> accountNames = new HashMap<>();
-        HashMap<String, LedgerAccount> syntheticAccounts = new HashMap<>();
-        LedgerAccount lastAccount = null, prevAccount = null;
-        boolean onlyStarred = Data.optShowOnlyStarred.get();
+        progress.setState(ProgressState.FINISHED);
+        publishProgress(progress);
+    }
+    private void retrieveTransactionListLegacy(List<LedgerAccount> accounts,
+                                               List<LedgerTransaction> transactions)
+            throws IOException, HTTPException {
+        Progress progress = Progress.indeterminate();
+        progress.setState(ProgressState.RUNNING);
+        progress.setTotal(expectedPostingsCount);
+        int maxTransactionId = -1;
+        HashMap<String, LedgerAccount> map = new HashMap<>();
+        LedgerAccount lastAccount = null;
+        ArrayList<LedgerAccount> syntheticAccounts = new ArrayList<>();
 
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "journal");
         http.setAllowUserInteraction(false);
         publishProgress(progress);
 
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "journal");
         http.setAllowUserInteraction(false);
         publishProgress(progress);
-        switch (http.getResponseCode()) {
-            case 200:
-                break;
-            default:
-                throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
-        }
+        if (http.getResponseCode() != 200)
+            throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
 
 
-        SQLiteDatabase db = App.getDatabase();
         try (InputStream resp = http.getInputStream()) {
             if (http.getResponseCode() != 200)
                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
         try (InputStream resp = http.getInputStream()) {
             if (http.getResponseCode() != 200)
                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
-            db.beginTransaction();
-            try {
-                prepareDbForRetrieval(db, profile);
-
-                int matchedTransactionsCount = 0;
-
-
-                ParserState state = ParserState.EXPECTING_ACCOUNT;
-                String line;
-                BufferedReader buf =
-                        new BufferedReader(new InputStreamReader(resp, StandardCharsets.UTF_8));
-
-                int processedTransactionCount = 0;
-                int transactionId = 0;
-                LedgerTransaction transaction = null;
-                LINES:
-                while ((line = buf.readLine()) != null) {
-                    throwIfCancelled();
-                    Matcher m;
-                    m = reComment.matcher(line);
-                    if (m.find()) {
-                        // TODO: comments are ignored for now
+
+            int matchedTransactionsCount = 0;
+
+            ParserState state = ParserState.EXPECTING_ACCOUNT;
+            String line;
+            BufferedReader buf =
+                    new BufferedReader(new InputStreamReader(resp, StandardCharsets.UTF_8));
+
+            int processedTransactionCount = 0;
+            int transactionId = 0;
+            LedgerTransaction transaction = null;
+            LINES:
+            while ((line = buf.readLine()) != null) {
+                throwIfCancelled();
+                Matcher m;
+                m = reComment.matcher(line);
+                if (m.find()) {
+                    // TODO: comments are ignored for now
 //                            Log.v("transaction-parser", "Ignoring comment");
 //                            Log.v("transaction-parser", "Ignoring comment");
-                        continue;
-                    }
-                    //L(String.format("State is %d", updating));
-                    switch (state) {
-                        case EXPECTING_ACCOUNT:
-                            if (line.equals("<h2>General Journal</h2>")) {
-                                state = ParserState.EXPECTING_TRANSACTION;
-                                L("→ expecting transaction");
-                                // commit the current transaction and start a new one
-                                // the account list in the UI should reflect the (committed)
-                                // state of the database
-                                db.setTransactionSuccessful();
-                                db.endTransaction();
-                                Data.accounts.setList(accountList);
-                                db.beginTransaction();
+                    continue;
+                }
+                //L(String.format("State is %d", updating));
+                switch (state) {
+                    case EXPECTING_ACCOUNT:
+                        if (line.equals("<h2>General Journal</h2>")) {
+                            state = ParserState.EXPECTING_TRANSACTION;
+                            L("→ expecting transaction");
+                            continue;
+                        }
+                        m = reAccountName.matcher(line);
+                        if (m.find()) {
+                            String acct_encoded = m.group(1);
+                            String accName = URLDecoder.decode(acct_encoded, "UTF-8");
+                            accName = accName.replace("\"", "");
+                            L(String.format("found account: %s", accName));
+
+                            lastAccount = map.get(accName);
+                            if (lastAccount != null) {
+                                L(String.format("ignoring duplicate account '%s'", accName));
                                 continue;
                             }
                                 continue;
                             }
-                            m = reAccountName.matcher(line);
-                            if (m.find()) {
-                                String acct_encoded = m.group(1);
-                                String acct_name = URLDecoder.decode(acct_encoded, "UTF-8");
-                                acct_name = acct_name.replace("\"", "");
-                                L(String.format("found account: %s", acct_name));
-
-                                prevAccount = lastAccount;
-                                lastAccount = profile.tryLoadAccount(db, acct_name);
-                                if (lastAccount == null) lastAccount = new LedgerAccount(acct_name);
-                                else lastAccount.removeAmounts();
-                                profile.storeAccount(db, lastAccount);
-
-                                if (prevAccount != null) prevAccount
-                                        .setHasSubAccounts(prevAccount.isParentOf(lastAccount));
-                                // make sure the parent account(s) are present,
-                                // synthesising them if necessary
-                                // this happens when the (missing-in-HTML) parent account has
-                                // only one child so we create a synthetic parent account record,
-                                // copying the amounts when child's amounts are parsed
-                                String parentName = lastAccount.getParentName();
-                                if (parentName != null) {
-                                    Stack<String> toAppend = new Stack<>();
-                                    while (parentName != null) {
-                                        if (accountNames.containsKey(parentName)) break;
-                                        toAppend.push(parentName);
-                                        parentName = new LedgerAccount(parentName).getParentName();
-                                    }
-                                    syntheticAccounts.clear();
-                                    while (!toAppend.isEmpty()) {
-                                        String aName = toAppend.pop();
-                                        LedgerAccount acc = profile.tryLoadAccount(db, aName);
-                                        if (acc == null) {
-                                            acc = new LedgerAccount(aName);
-                                            acc.setHiddenByStar(lastAccount.isHiddenByStar());
-                                            acc.setExpanded(!lastAccount.hasSubAccounts() ||
-                                                            lastAccount.isExpanded());
-                                        }
-                                        acc.setHasSubAccounts(true);
-                                        acc.removeAmounts();    // filled below when amounts are parsed
-                                        if ((!onlyStarred || !acc.isHiddenByStar()) &&
-                                            acc.isVisible(accountList)) accountList.add(acc);
-                                        L(String.format("gap-filling with %s", aName));
-                                        accountNames.put(aName, null);
-                                        profile.storeAccount(db, acc);
-                                        syntheticAccounts.put(aName, acc);
-                                    }
-                                }
+                            String parentAccountName = LedgerAccount.extractParentName(accName);
+                            LedgerAccount parentAccount;
+                            if (parentAccountName != null) {
+                                parentAccount = ensureAccountExists(parentAccountName, map,
+                                        syntheticAccounts);
+                            }
+                            else {
+                                parentAccount = null;
+                            }
+                            lastAccount = new LedgerAccount(accName, parentAccount);
 
 
-                                if ((!onlyStarred || !lastAccount.isHiddenByStar()) &&
-                                    lastAccount.isVisible(accountList))
-                                    accountList.add(lastAccount);
-                                accountNames.put(acct_name, null);
+                            accounts.add(lastAccount);
+                            map.put(accName, lastAccount);
 
 
-                                state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
-                                L("→ expecting account amount");
-                            }
-                            break;
-
-                        case EXPECTING_ACCOUNT_AMOUNT:
-                            m = reAccountValue.matcher(line);
-                            boolean match_found = false;
-                            while (m.find()) {
-                                throwIfCancelled();
-
-                                match_found = true;
-                                String value = m.group(1);
-                                String currency = m.group(2);
-                                if (currency == null) currency = "";
-
-                                {
-                                    Matcher tmpM = reDecimalComma.matcher(value);
-                                    if (tmpM.find()) {
-                                        value = value.replace(".", "");
-                                        value = value.replace(',', '.');
-                                    }
-
-                                    tmpM = reDecimalPoint.matcher(value);
-                                    if (tmpM.find()) {
-                                        value = value.replace(",", "");
-                                        value = value.replace(" ", "");
-                                    }
+                            state = ParserState.EXPECTING_ACCOUNT_AMOUNT;
+                            L("→ expecting account amount");
+                        }
+                        break;
+
+                    case EXPECTING_ACCOUNT_AMOUNT:
+                        m = reAccountValue.matcher(line);
+                        boolean match_found = false;
+                        while (m.find()) {
+                            throwIfCancelled();
+
+                            match_found = true;
+                            String value = Objects.requireNonNull(m.group(1));
+                            String currency = m.group(2);
+                            if (currency == null)
+                                currency = "";
+
+                            {
+                                Matcher tmpM = reDecimalComma.matcher(value);
+                                if (tmpM.find()) {
+                                    value = value.replace(".", "");
+                                    value = value.replace(',', '.');
                                 }
                                 }
-                                L("curr=" + currency + ", value=" + value);
-                                final float val = Float.parseFloat(value);
-                                profile.storeAccountValue(db, lastAccount.getName(), currency, val);
-                                lastAccount.addAmount(val, currency);
-                                for (LedgerAccount syn : syntheticAccounts.values()) {
-                                    L(String.format(Locale.ENGLISH, "propagating %s %1.2f to %s",
-                                            currency, val, syn.getName()));
-                                    syn.addAmount(val, currency);
-                                    profile.storeAccountValue(db, syn.getName(), currency, val);
+
+                                tmpM = reDecimalPoint.matcher(value);
+                                if (tmpM.find()) {
+                                    value = value.replace(",", "");
+                                    value = value.replace(" ", "");
                                 }
                             }
                                 }
                             }
-
-                            if (match_found) {
-                                syntheticAccounts.clear();
-                                state = ParserState.EXPECTING_ACCOUNT;
-                                L("→ expecting account");
+                            L("curr=" + currency + ", value=" + value);
+                            final float val = Float.parseFloat(value);
+                            lastAccount.addAmount(val, currency);
+                            for (LedgerAccount syn : syntheticAccounts) {
+                                L(String.format(Locale.ENGLISH, "propagating %s %1.2f to %s",
+                                        currency, val, syn.getName()));
+                                syn.addAmount(val, currency);
                             }
                             }
+                        }
 
 
-                            break;
-
-                        case EXPECTING_TRANSACTION:
-                            if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
-                            m = reTransactionStart.matcher(line);
-                            if (m.find()) {
-                                transactionId = Integer.valueOf(m.group(1));
-                                state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
-                                L(String.format(Locale.ENGLISH,
-                                        "found transaction %d → expecting description",
-                                        transactionId));
-                                progress.setProgress(++processedTransactionCount);
-                                if (maxTransactionId < transactionId)
-                                    maxTransactionId = transactionId;
-                                if ((progress.getTotal() == Progress.INDETERMINATE) ||
-                                    (progress.getTotal() < transactionId))
-                                    progress.setTotal(transactionId);
-                                publishProgress(progress);
-                            }
-                            m = reEnd.matcher(line);
-                            if (m.find()) {
-                                L("--- transaction value complete ---");
-                                break LINES;
+                        if (match_found) {
+                            syntheticAccounts.clear();
+                            state = ParserState.EXPECTING_ACCOUNT;
+                            L("→ expecting account");
+                        }
+
+                        break;
+
+                    case EXPECTING_TRANSACTION:
+                        if (!line.isEmpty() && (line.charAt(0) == ' '))
+                            continue;
+                        m = reTransactionStart.matcher(line);
+                        if (m.find()) {
+                            transactionId = Integer.parseInt(Objects.requireNonNull(m.group(1)));
+                            state = ParserState.EXPECTING_TRANSACTION_DESCRIPTION;
+                            L(String.format(Locale.ENGLISH,
+                                    "found transaction %d → expecting description", transactionId));
+                            progress.setProgress(++processedTransactionCount);
+                            if (maxTransactionId < transactionId)
+                                maxTransactionId = transactionId;
+                            if ((progress.isIndeterminate()) ||
+                                (progress.getTotal() < transactionId))
+                                progress.setTotal(transactionId);
+                            publishProgress(progress);
+                        }
+                        m = reEnd.matcher(line);
+                        if (m.find()) {
+                            L("--- transaction value complete ---");
+                            break LINES;
+                        }
+                        break;
+
+                    case EXPECTING_TRANSACTION_DESCRIPTION:
+                        if (!line.isEmpty() && (line.charAt(0) == ' '))
+                            continue;
+                        m = reTransactionDescription.matcher(line);
+                        if (m.find()) {
+                            if (transactionId == 0)
+                                throw new TransactionParserException(
+                                        "Transaction Id is 0 while expecting description");
+
+                            String date = Objects.requireNonNull(m.group(1));
+                            try {
+                                int equalsIndex = date.indexOf('=');
+                                if (equalsIndex >= 0)
+                                    date = date.substring(equalsIndex + 1);
+                                transaction =
+                                        new LedgerTransaction(transactionId, date, m.group(2));
                             }
                             }
-                            break;
-
-                        case EXPECTING_TRANSACTION_DESCRIPTION:
-                            if (!line.isEmpty() && (line.charAt(0) == ' ')) continue;
-                            m = reTransactionDescription.matcher(line);
-                            if (m.find()) {
-                                if (transactionId == 0) throw new TransactionParserException(
-                                        "Transaction Id is 0 while expecting " + "description");
-
-                                String date = m.group(1);
-                                try {
-                                    int equalsIndex = date.indexOf('=');
-                                    if (equalsIndex >= 0) date = date.substring(equalsIndex + 1);
-                                    transaction =
-                                            new LedgerTransaction(transactionId, date, m.group(2));
-                                }
-                                catch (ParseException e) {
-                                    e.printStackTrace();
-                                    return String.format("Error parsing date '%s'", date);
-                                }
-                                state = ParserState.EXPECTING_TRANSACTION_DETAILS;
-                                L(String.format(Locale.ENGLISH,
-                                        "transaction %d created for %s (%s) →" +
-                                        " expecting details", transactionId, date, m.group(2)));
+                            catch (ParseException e) {
+                                throw new TransactionParserException(
+                                        String.format("Error parsing date '%s'", date));
                             }
                             }
-                            break;
-
-                        case EXPECTING_TRANSACTION_DETAILS:
-                            if (line.isEmpty()) {
-                                // transaction data collected
-                                if (transaction.existsInDb(db)) {
-                                    profile.markTransactionAsPresent(db, transaction);
-                                    matchedTransactionsCount++;
-
-                                    if (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT) {
-                                        profile.markTransactionsBeforeTransactionAsPresent(db,
-                                                transaction);
-                                        progress.setTotal(progress.getProgress());
-                                        publishProgress(progress);
-                                        break LINES;
-                                    }
-                                }
-                                else {
-                                    profile.storeTransaction(db, transaction);
-                                    matchedTransactionsCount = 0;
-                                    progress.setTotal(maxTransactionId);
-                                }
+                            state = ParserState.EXPECTING_TRANSACTION_DETAILS;
+                            L(String.format(Locale.ENGLISH,
+                                    "transaction %d created for %s (%s) →" + " expecting details",
+                                    transactionId, date, m.group(2)));
+                        }
+                        break;
 
 
-                                state = ParserState.EXPECTING_TRANSACTION;
-                                L(String.format("transaction %s saved → expecting transaction",
-                                        transaction.getId()));
-                                transaction.finishLoading();
+                    case EXPECTING_TRANSACTION_DETAILS:
+                        if (line.isEmpty()) {
+                            // transaction data collected
+
+                            transaction.finishLoading();
+                            transactions.add(transaction);
+
+                            state = ParserState.EXPECTING_TRANSACTION;
+                            L(String.format("transaction %s parsed → expecting transaction",
+                                    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
 //                                            if (transactionId == 1) {
 
 // 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
 //                                            if (transactionId == 1) {
-//                                                L("This was the initial transaction. Terminating " +
+//                                                L("This was the initial transaction.
+//                                                Terminating " +
 //                                                  "parser");
 //                                                break LINES;
 //                                            }
 //                                                  "parser");
 //                                                break LINES;
 //                                            }
+                        }
+                        else {
+                            LedgerTransactionAccount lta = parseTransactionAccountLine(line);
+                            if (lta != null) {
+                                transaction.addAccount(lta);
+                                L(String.format(Locale.ENGLISH, "%d: %s = %s",
+                                        transaction.getLedgerId(), lta.getAccountName(),
+                                        lta.getAmount()));
                             }
                             }
-                            else {
-                                m = reTransactionDetails.matcher(line);
-                                if (m.find()) {
-                                    String acc_name = m.group(1);
-                                    String amount = m.group(2);
-                                    String currency = m.group(3);
-                                    if (currency == null) currency = "";
-                                    amount = amount.replace(',', '.');
-                                    transaction.addAccount(new LedgerTransactionAccount(acc_name,
-                                            Float.valueOf(amount), currency));
-                                    L(String.format(Locale.ENGLISH, "%d: %s = %s",
-                                            transaction.getId(), acc_name, amount));
-                                }
-                                else throw new IllegalStateException(
-                                        String.format("Can't parse transaction %d " + "details: %s",
+                            else
+                                throw new IllegalStateException(
+                                        String.format("Can't parse transaction %d details: %s",
                                                 transactionId, line));
                                                 transactionId, line));
-                            }
-                            break;
-                        default:
-                            throw new RuntimeException(
-                                    String.format("Unknown parser updating %s", state.name()));
-                    }
+                        }
+                        break;
+                    default:
+                        throw new RuntimeException(
+                                String.format("Unknown parser updating %s", state.name()));
                 }
                 }
+            }
 
 
-                throwIfCancelled();
-
-                profile.deleteNotPresentTransactions(db);
-                db.setTransactionSuccessful();
-
-                profile.setLastUpdateStamp();
+            throwIfCancelled();
+        }
+    }
+    @NonNull
+    public LedgerAccount ensureAccountExists(String accountName, HashMap<String, LedgerAccount> map,
+                                             ArrayList<LedgerAccount> createdAccounts) {
+        LedgerAccount acc = map.get(accountName);
+
+        if (acc != null)
+            return acc;
+
+        String parentName = LedgerAccount.extractParentName(accountName);
+        LedgerAccount parentAccount;
+        if (parentName != null) {
+            parentAccount = ensureAccountExists(parentName, map, createdAccounts);
+        }
+        else {
+            parentAccount = null;
+        }
 
 
-                return null;
+        acc = new LedgerAccount(accountName, parentAccount);
+        createdAccounts.add(acc);
+        return acc;
+    }
+    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);
             }
             }
-            finally {
-                db.endTransaction();
+            catch (JsonParseException | RuntimeJsonMappingException e) {
+                Logger.debug("json",
+                        String.format(Locale.US, "Error during account list retrieval using API %s",
+                                ver.getDescription()), e);
             }
             }
+
         }
         }
-    }
-    private void prepareDbForRetrieval(SQLiteDatabase db, MobileLedgerProfile profile) {
-        db.execSQL("UPDATE transactions set keep=0 where profile=?",
-                new String[]{profile.getUuid()});
-        db.execSQL("update account_values set keep=0 where profile=?;",
-                new String[]{profile.getUuid()});
-        db.execSQL("update accounts set keep=0 where profile=?;", new String[]{profile.getUuid()});
-    }
-    private boolean retrieveAccountList() throws IOException, HTTPException {
-        Progress progress = new Progress();
 
 
+        throw new ApiNotSupportedException();
+    }
+    private List<LedgerAccount> retrieveAccountListForVersion(API version)
+            throws IOException, HTTPException {
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
         http.setAllowUserInteraction(false);
         switch (http.getResponseCode()) {
             case 200:
                 break;
             case 404:
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "accounts");
         http.setAllowUserInteraction(false);
         switch (http.getResponseCode()) {
             case 200:
                 break;
             case 404:
-                return false;
+                return null;
             default:
                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
         }
             default:
                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
         }
-        publishProgress(progress);
-        SQLiteDatabase db = App.getDatabase();
-        ArrayList<LedgerAccount> accountList = new ArrayList<>();
-        boolean listFilledOK = false;
+        publishProgress(Progress.indeterminate());
+        ArrayList<LedgerAccount> list = new ArrayList<>();
+        HashMap<String, LedgerAccount> map = new HashMap<>();
+        throwIfCancelled();
         try (InputStream resp = http.getInputStream()) {
         try (InputStream resp = http.getInputStream()) {
+            throwIfCancelled();
             if (http.getResponseCode() != 200)
                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
 
             if (http.getResponseCode() != 200)
                 throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
 
-            db.beginTransaction();
-            try {
-                profile.markAccountsAsNotPresent(db);
-
-                AccountListParser parser = new AccountListParser(resp);
+            AccountListParser parser = AccountListParser.forApiVersion(version, resp);
+            expectedPostingsCount = 0;
 
 
-                LedgerAccount prevAccount = null;
-
-                while (true) {
-                    throwIfCancelled();
-                    ParsedLedgerAccount parsedAccount = parser.nextAccount();
-                    if (parsedAccount == null) break;
-
-                    LedgerAccount acc = profile.tryLoadAccount(db, parsedAccount.getAname());
-                    if (acc == null) acc = new LedgerAccount(parsedAccount.getAname());
-                    else acc.removeAmounts();
-
-                    profile.storeAccount(db, acc);
-                    String lastCurrency = null;
-                    float lastCurrencyAmount = 0;
-                    for (ParsedBalance b : parsedAccount.getAibalance()) {
-                        final String currency = b.getAcommodity();
-                        final float amount = b.getAquantity().asFloat();
-                        if (currency.equals(lastCurrency)) lastCurrencyAmount += amount;
-                        else {
-                            if (lastCurrency != null) {
-                                profile.storeAccountValue(db, acc.getName(), lastCurrency,
-                                        lastCurrencyAmount);
-                                acc.addAmount(lastCurrencyAmount, lastCurrency);
-                            }
-                            lastCurrency = currency;
-                            lastCurrencyAmount = amount;
-                        }
-                    }
-                    if (lastCurrency != null) {
-                        profile.storeAccountValue(db, acc.getName(), lastCurrency,
-                                lastCurrencyAmount);
-                        acc.addAmount(lastCurrencyAmount, lastCurrency);
-                    }
-
-                    if (acc.isVisible(accountList)) accountList.add(acc);
+            while (true) {
+                throwIfCancelled();
+                LedgerAccount acc = parser.nextAccount(this, map);
+                if (acc == null)
+                    break;
+                list.add(acc);
+            }
+            throwIfCancelled();
 
 
-                    if (prevAccount != null) {
-                        prevAccount.setHasSubAccounts(
-                                acc.getName().startsWith(prevAccount.getName() + ":"));
-                    }
+            Logger.warn("accounts",
+                    String.format(Locale.US, "Got %d accounts using protocol %s", list.size(),
+                            version.getDescription()));
+        }
 
 
-                    prevAccount = acc;
-                }
-                throwIfCancelled();
+        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);
+        }
 
 
-                profile.deleteNotPresentAccounts(db);
-                throwIfCancelled();
-                db.setTransactionSuccessful();
-                listFilledOK = true;
+    }
+    private List<LedgerTransaction> retrieveTransactionListAnyVersion()
+            throws ApiNotSupportedException {
+        for (API ver : API.allVersions) {
+            try {
+                return retrieveTransactionListForVersion(ver);
             }
             }
-            finally {
-                db.endTransaction();
+            catch (Exception e) {
+                Logger.debug("json", String.format(Locale.US,
+                        "Error during transaction list retrieval using API %s",
+                        ver.getDescription()), e);
             }
             }
+
         }
         }
-        // should not be set in the DB transaction, because of a possible deadlock
-        // with the main and DbOpQueueRunner threads
-        if (listFilledOK) Data.accounts.setList(accountList);
 
 
-        return true;
+        throw new ApiNotSupportedException();
     }
     }
-    private boolean retrieveTransactionList() throws IOException, ParseException, HTTPException {
+    private List<LedgerTransaction> retrieveTransactionListForVersion(API apiVersion)
+            throws IOException, ParseException, HTTPException {
         Progress progress = new Progress();
         Progress progress = new Progress();
-        int maxTransactionId = Progress.INDETERMINATE;
+        progress.setTotal(expectedPostingsCount);
 
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
         http.setAllowUserInteraction(false);
 
         HttpURLConnection http = NetworkUtil.prepareConnection(profile, "transactions");
         http.setAllowUserInteraction(false);
@@ -507,173 +491,267 @@ public class RetrieveTransactionsTask
             case 200:
                 break;
             case 404:
             case 200:
                 break;
             case 404:
-                return false;
+                return null;
             default:
                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
         }
             default:
                 throw new HTTPException(http.getResponseCode(), http.getResponseMessage());
         }
-        SQLiteDatabase db = App.getDatabase();
+        ArrayList<LedgerTransaction> trList = new ArrayList<>();
         try (InputStream resp = http.getInputStream()) {
         try (InputStream resp = http.getInputStream()) {
-            if (http.getResponseCode() != 200)
-                throw new IOException(String.format("HTTP error %d", http.getResponseCode()));
             throwIfCancelled();
             throwIfCancelled();
-            db.beginTransaction();
-            try {
-                profile.markTransactionsAsNotPresent(db);
-
-                int matchedTransactionsCount = 0;
-                TransactionListParser parser = new TransactionListParser(resp);
-
-                int processedTransactionCount = 0;
-
-                DetectedTransactionOrder transactionOrder = DetectedTransactionOrder.UNKNOWN;
-                int orderAccumulator = 0;
-                int lastTransactionId = 0;
-
-                while (true) {
-                    throwIfCancelled();
-                    ParsedLedgerTransaction parsedTransaction = parser.nextTransaction();
-                    throwIfCancelled();
-                    if (parsedTransaction == null) break;
-
-                    LedgerTransaction transaction = parsedTransaction.asLedgerTransaction();
-                    if (transaction.getId() > lastTransactionId) orderAccumulator++;
-                    else orderAccumulator--;
-                    lastTransactionId = transaction.getId();
-                    if (transactionOrder == DetectedTransactionOrder.UNKNOWN) {
-                        if (orderAccumulator > 30) {
-                            transactionOrder = DetectedTransactionOrder.FILE;
-                            debug("rtt", String.format(Locale.ENGLISH,
-                                    "Detected native file order after %d transactions (factor %d)",
-                                    processedTransactionCount, orderAccumulator));
-                            progress.setTotal(Data.transactions.size());
-                        }
-                        else if (orderAccumulator < -30) {
-                            transactionOrder = DetectedTransactionOrder.REVERSE_CHRONOLOGICAL;
-                            debug("rtt", String.format(Locale.ENGLISH,
-                                    "Detected reverse chronological order after %d transactions (factor %d)",
-                                    processedTransactionCount, orderAccumulator));
-                        }
-                    }
-
-                    if (transaction.existsInDb(db)) {
-                        profile.markTransactionAsPresent(db, transaction);
-                        matchedTransactionsCount++;
-
-                        if ((transactionOrder == DetectedTransactionOrder.REVERSE_CHRONOLOGICAL) &&
-                            (matchedTransactionsCount == MATCHING_TRANSACTIONS_LIMIT))
-                        {
-                            profile.markTransactionsBeforeTransactionAsPresent(db, transaction);
-                            progress.setTotal(progress.getProgress());
-                            publishProgress(progress);
-                            db.setTransactionSuccessful();
-                            profile.setLastUpdateStamp();
-                            return true;
-                        }
-                    }
-                    else {
-                        profile.storeTransaction(db, transaction);
-                        matchedTransactionsCount = 0;
-                        progress.setTotal(maxTransactionId);
-                    }
 
 
+            TransactionListParser parser = TransactionListParser.forApiVersion(apiVersion, resp);
 
 
-                    if ((transactionOrder != DetectedTransactionOrder.UNKNOWN) &&
-                        ((progress.getTotal() == Progress.INDETERMINATE) ||
-                         (progress.getTotal() < transaction.getId())))
-                        progress.setTotal(transaction.getId());
-
-                    progress.setProgress(++processedTransactionCount);
-                    publishProgress(progress);
-                }
+            int processedPostings = 0;
 
 
+            while (true) {
                 throwIfCancelled();
                 throwIfCancelled();
-                profile.deleteNotPresentTransactions(db);
+                LedgerTransaction transaction = parser.nextTransaction();
                 throwIfCancelled();
                 throwIfCancelled();
-                db.setTransactionSuccessful();
-                profile.setLastUpdateStamp();
-            }
-            finally {
-                db.endTransaction();
+                if (transaction == null)
+                    break;
+
+                trList.add(transaction);
+
+                progress.setProgress(processedPostings += transaction.getAccounts()
+                                                                     .size());
+//                Logger.debug("trParser",
+//                        String.format(Locale.US, "Parsed transaction %d - %s", transaction
+//                        .getId(),
+//                                transaction.getDescription()));
+//                for (LedgerTransactionAccount acc : transaction.getAccounts()) {
+//                    Logger.debug("trParser",
+//                            String.format(Locale.US, "  %s", acc.getAccountName()));
+//                }
+                publishProgress(progress);
             }
             }
+
+            throwIfCancelled();
+
+            Logger.warn("transactions",
+                    String.format(Locale.US, "Got %d transactions using protocol %s", trList.size(),
+                            apiVersion.getDescription()));
         }
 
         }
 
-        return true;
+        // 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 Long.compare(o2.getLedgerId(), o1.getLedgerId());
+        });
+        return trList;
     }
 
     @SuppressLint("DefaultLocale")
     @Override
     }
 
     @SuppressLint("DefaultLocale")
     @Override
-    protected String doInBackground(Void... params) {
+    public void run() {
         Data.backgroundTaskStarted();
         Data.backgroundTaskStarted();
+        List<LedgerAccount> accounts;
+        List<LedgerTransaction> transactions;
         try {
         try {
-            if (!retrieveAccountList() || !retrieveTransactionList())
-                return retrieveTransactionListLegacy();
-            return null;
+            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);
+            }
+
+            new AccountAndTransactionListSaver(accounts, transactions).start();
+
+            Data.lastUpdateDate.postValue(new Date());
+
+            finish(new Result(null));
         }
         catch (MalformedURLException e) {
             e.printStackTrace();
         }
         catch (MalformedURLException e) {
             e.printStackTrace();
-            return "Invalid server URL";
+            finish(new Result("Invalid server URL"));
         }
         catch (HTTPException e) {
             e.printStackTrace();
         }
         catch (HTTPException e) {
             e.printStackTrace();
-            return 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();
         }
         catch (IOException e) {
             e.printStackTrace();
-            return 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();
         }
         catch (ParseException e) {
             e.printStackTrace();
-            return "Network error";
+            finish(new Result("Network error"));
         }
         catch (OperationCanceledException e) {
         }
         catch (OperationCanceledException e) {
+            Logger.debug("RTT", "Retrieval was cancelled", e);
+            finish(new Result(null));
+        }
+        catch (ApiNotSupportedException e) {
             e.printStackTrace();
             e.printStackTrace();
-            return "Operation cancelled";
+            finish(new Result("Server version not supported"));
         }
         finally {
             Data.backgroundTaskFinished();
         }
     }
         }
         finally {
             Data.backgroundTaskFinished();
         }
     }
-    private MainActivity getContext() {
-        return contextRef.get();
-    }
-    private void throwIfCancelled() {
-        if (isCancelled()) throw new OperationCanceledException(null);
+    public void throwIfCancelled() {
+        if (isInterrupted())
+            throw new OperationCanceledException(null);
     }
     }
-    enum DetectedTransactionOrder {UNKNOWN, REVERSE_CHRONOLOGICAL, FILE}
-
     private enum ParserState {
     private enum ParserState {
-        EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_JOURNAL, EXPECTING_TRANSACTION,
+        EXPECTING_ACCOUNT, EXPECTING_ACCOUNT_AMOUNT, EXPECTING_TRANSACTION,
         EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS
     }
 
         EXPECTING_TRANSACTION_DESCRIPTION, EXPECTING_TRANSACTION_DETAILS
     }
 
-    public class Progress {
-        public static final int INDETERMINATE = -1;
+    public enum ProgressState {STARTING, RUNNING, FINISHED}
+
+    public static class Progress {
         private int progress;
         private int total;
         private int progress;
         private int total;
+        private ProgressState state = ProgressState.RUNNING;
+        private String error = null;
+        private boolean indeterminate;
         Progress() {
         Progress() {
-            this(INDETERMINATE, INDETERMINATE);
+            indeterminate = true;
         }
         Progress(int progress, int total) {
         }
         Progress(int progress, int total) {
+            this.indeterminate = false;
             this.progress = progress;
             this.total = total;
         }
             this.progress = progress;
             this.total = total;
         }
+        public static Progress indeterminate() {
+            return new Progress();
+        }
+        public static Progress finished(String error) {
+            Progress p = new Progress();
+            p.setState(ProgressState.FINISHED);
+            p.setError(error);
+            return p;
+        }
         public int getProgress() {
         public int getProgress() {
+            ensureState(ProgressState.RUNNING);
             return progress;
         }
         protected void setProgress(int progress) {
             this.progress = progress;
             return progress;
         }
         protected void setProgress(int progress) {
             this.progress = progress;
+            this.state = ProgressState.RUNNING;
         }
         public int getTotal() {
         }
         public int getTotal() {
+            ensureState(ProgressState.RUNNING);
             return total;
         }
         protected void setTotal(int total) {
             this.total = total;
             return total;
         }
         protected void setTotal(int total) {
             this.total = total;
+            state = ProgressState.RUNNING;
+            indeterminate = total == -1;
+        }
+        private void ensureState(ProgressState wanted) {
+            if (state != wanted)
+                throw new IllegalStateException(
+                        String.format("Bad state: %s, expected %s", state, wanted));
+        }
+        public ProgressState getState() {
+            return state;
+        }
+        public void setState(ProgressState state) {
+            this.state = state;
+        }
+        public String getError() {
+            ensureState(ProgressState.FINISHED);
+            return error;
+        }
+        public void setError(String error) {
+            this.error = error;
+            state = ProgressState.FINISHED;
+        }
+        public boolean isIndeterminate() {
+            return indeterminate;
+        }
+        public void setIndeterminate(boolean indeterminate) {
+            this.indeterminate = indeterminate;
         }
     }
 
         }
     }
 
-    private class TransactionParserException extends IllegalStateException {
+    private static class TransactionParserException extends IllegalStateException {
         TransactionParserException(String message) {
             super(message);
         }
     }
         TransactionParserException(String message) {
             super(message);
         }
     }
+
+    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;
+        Result(String error) {
+            this.error = error;
+        }
+        Result(List<LedgerAccount> accounts, List<LedgerTransaction> transactions) {
+            this.accounts = accounts;
+            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())));
+        }
+    }
 }
 }
index 5170b9337a362dbcafc55cd6e66360056521157a..0e700cca9a510be8e210fafccbe548b88c2a427a 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright © 2019 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
  * This file is part of MoLe.
  * 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;
 
 
 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.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.Globals;
 import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
 import net.ktnx.mobileledger.utils.NetworkUtil;
 import net.ktnx.mobileledger.utils.NetworkUtil;
+import net.ktnx.mobileledger.utils.SimpleDate;
 import net.ktnx.mobileledger.utils.UrlEncodedFormData;
 
 import java.io.BufferedReader;
 import net.ktnx.mobileledger.utils.UrlEncodedFormData;
 
 import java.io.BufferedReader;
@@ -41,68 +41,48 @@ import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.nio.charset.StandardCharsets;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 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 TaskCallback taskCallback;
+    private final Profile mProfile;
+    private final boolean simulate;
+    private final LedgerTransaction transaction;
     protected String error;
     private String token;
     private String session;
     protected String error;
     private String token;
     private String session;
-    private LedgerTransaction ltr;
-    private MobileLedgerProfile mProfile;
-    private boolean simulate = false;
 
 
-    public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile,
-                               boolean simulate) {
+    public SendTransactionTask(TaskCallback callback, Profile profile,
+                               LedgerTransaction transaction, boolean simulate) {
         taskCallback = callback;
         mProfile = profile;
         taskCallback = callback;
         mProfile = profile;
+        this.transaction = transaction;
         this.simulate = simulate;
     }
         this.simulate = simulate;
     }
-    public SendTransactionTask(TaskCallback callback, MobileLedgerProfile profile) {
-        taskCallback = callback;
-        mProfile = profile;
-        simulate = false;
-    }
-    private boolean send_1_15_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_15.ParsedLedgerTransaction jsonTransaction =
-                net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.fromLedgerTransaction(ltr);
-        ObjectMapper mapper = new ObjectMapper();
-        ObjectWriter writer =
-                mapper.writerFor(net.ktnx.mobileledger.json.v1_15.ParsedLedgerTransaction.class);
-        String body = writer.writeValueAsString(jsonTransaction);
-
-        return sendRequest(http, body);
-    }
-    private boolean send_1_14_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", "*/*");
 
         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(ltr);
-        ObjectMapper mapper = new ObjectMapper();
-        ObjectWriter writer =
-                mapper.writerFor(net.ktnx.mobileledger.json.v1_14.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 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 {
         if (simulate) {
             debug("network", "The request would be: " + body);
             try {
@@ -114,7 +94,7 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
                 Logger.debug("network", ex.toString());
             }
 
                 Logger.debug("network", ex.toString());
             }
 
-            return true;
+            return;
         }
 
         byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
         }
 
         byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8);
@@ -130,8 +110,8 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
             req.write(bodyBytes);
 
             final int responseCode = http.getResponseCode();
             req.write(bodyBytes);
 
             final int responseCode = http.getResponseCode();
-            debug("network",
-                    String.format("Response: %d %s", responseCode, http.getResponseMessage()));
+            debug("network", String.format(Locale.US, "Response: %d %s", responseCode,
+                    http.getResponseMessage()));
 
             try (InputStream resp = http.getErrorStream()) {
 
 
             try (InputStream resp = http.getErrorStream()) {
 
@@ -140,8 +120,24 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
                     case 201:
                         break;
                     case 400:
                     case 201:
                         break;
                     case 400:
-                    case 405:
-                        return false; // will cause a retry with the legacy method
+                    case 405: {
+                        BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
+                        StringBuilder errorLines = new StringBuilder();
+                        int count = 0;
+                        while (count <= 5) {
+                            String line = reader.readLine();
+                            if (line == null)
+                                break;
+                            Logger.debug("network", line);
+
+                            if (errorLines.length() != 0)
+                                errorLines.append("\n");
+
+                            errorLines.append(line);
+                            count++;
+                        }
+                        throw new ApiNotSupportedException(errorLines.toString());
+                    }
                     default:
                         BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
                         String line = reader.readLine();
                     default:
                         BufferedReader reader = new BufferedReader(new InputStreamReader(resp));
                         String line = reader.readLine();
@@ -151,8 +147,6 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
                 }
             }
         }
                 }
             }
         }
-
-        return true;
     }
     private boolean legacySendOK() throws IOException {
         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
     }
     private boolean legacySendOK() throws IOException {
         HttpURLConnection http = NetworkUtil.prepareConnection(mProfile, "add");
@@ -170,14 +164,13 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
         if (token != null)
             params.addPair("_token", token);
 
         if (token != null)
             params.addPair("_token", token);
 
-        Date transactionDate = ltr.getDate();
-        if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
-        }
+        SimpleDate transactionDate = transaction.getDateIfAny();
+        if (transactionDate == null)
+            transactionDate = SimpleDate.today();
 
         params.addPair("date", Globals.formatLedgerDate(transactionDate));
 
         params.addPair("date", Globals.formatLedgerDate(transactionDate));
-        params.addPair("description", ltr.getDescription());
-        for (LedgerTransactionAccount acc : ltr.getAccounts()) {
+        params.addPair("description", transaction.getDescription());
+        for (LedgerTransactionAccount acc : transaction.getAccounts()) {
             params.addPair("account", acc.getAccountName());
             if (acc.isAmountSet())
                 params.addPair("amount", String.format(Locale.US, "%1.2f", acc.getAmount()));
             params.addPair("account", acc.getAccountName());
             if (acc.isAmountSet())
                 params.addPair("amount", String.format(Locale.US, "%1.2f", acc.getAmount()));
@@ -250,99 +243,65 @@ public class SendTransactionTask extends AsyncTask<LedgerTransaction, Void, Void
         }
     }
     @Override
         }
     }
     @Override
-    protected Void doInBackground(LedgerTransaction... ledgerTransactions) {
+    public void run() {
         error = null;
         try {
         error = null;
         try {
-            ltr = ledgerTransactions[0];
-
-            switch (mProfile.getApiVersion()) {
+            final API profileApiVersion = API.valueOf(mProfile.getApiVersion());
+            switch (profileApiVersion) {
                 case auto:
                 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;
                     }
                     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:
                     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();
         }
 
             e.printStackTrace();
             error = e.getMessage();
         }
 
-        return null;
+        Misc.onMainThread(() -> taskCallback.onTransactionSaveDone(error, transaction));
     }
     private void legacySendOkWithRetry() throws IOException {
         int tried = 0;
         while (!legacySendOK()) {
             tried++;
             if (tried >= 2)
     }
     private void legacySendOkWithRetry() throws IOException {
         int tried = 0;
         while (!legacySendOK()) {
             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 SparseArray<API> map = new SparseArray<>();
-
-        static {
-            for (API item : API.values()) {
-                map.put(item.value, item);
+                throw new IOException(String.format("aborting after %d tries", tried));
+            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
index c1db43dadbe666c5cf6e5040cf7b3caf6a030bde..3f065e0b7b1d1e50088909ef7b9cb444f6518722 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -18,5 +18,5 @@
 package net.ktnx.mobileledger.async;
 
 public interface TaskCallback {
 package net.ktnx.mobileledger.async;
 
 public interface TaskCallback {
-    void done(String error);
+    void onTransactionSaveDone(String error, Object args);
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java b/app/src/main/java/net/ktnx/mobileledger/async/TransactionAccumulator.java
new file mode 100644 (file)
index 0000000..d3131f7
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 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 String boldAccountName;
+    private final String accumulateAccount;
+    private final HashMap<String, BigDecimal> runningTotal = new HashMap<>();
+    private SimpleDate earliestDate, latestDate;
+    private SimpleDate lastDate;
+    private int transactionCount = 0;
+    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) {
+        transactionCount++;
+
+        // first item
+        if (null == earliestDate)
+            earliestDate = date;
+        latestDate = date;
+
+        if (lastDate != null && !date.equals(lastDate)) {
+            boolean showMonth = date.month != lastDate.month || date.year != lastDate.year;
+            list.add(1, new TransactionListItem(lastDate, showMonth));
+        }
+
+        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;
+    }
+    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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/TransactionDateFinder.java b/app/src/main/java/net/ktnx/mobileledger/async/TransactionDateFinder.java
new file mode 100644 (file)
index 0000000..18cea62
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * 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.model.TransactionListItem;
+import net.ktnx.mobileledger.ui.MainModel;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+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
+    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(
+                model.getDisplayedTransactions()
+                     .getValue());
+        final int transactionCount = transactions.size();
+        Logger.debug("go-to-date",
+                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)
+            found = -1 - found;
+
+        model.foundTransactionItemIndex.postValue(found);
+    }
+
+    static class TransactionListItemComparator implements Comparator<TransactionListItem> {
+        @Override
+        public int compare(@NotNull TransactionListItem a, @NotNull TransactionListItem b) {
+            final TransactionListItem.Type aType = a.getType();
+            if (aType == TransactionListItem.Type.HEADER)
+                return +1;
+            final TransactionListItem.Type bType = b.getType();
+            if (bType == TransactionListItem.Type.HEADER)
+                return -1;
+            final SimpleDate aDate = a.getDate();
+            final SimpleDate bDate = b.getDate();
+            int res = aDate.compareTo(bDate);
+            if (res != 0)
+                return -res;    // transactions are reverse sorted by date
+
+            if (aType == TransactionListItem.Type.DELIMITER) {
+                if (bType == TransactionListItem.Type.DELIMITER)
+                    return 0;
+                else
+                    return -1;
+            }
+            else {
+                if (bType == TransactionListItem.Type.DELIMITER)
+                    return +1;
+                else
+                    return 0;
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/UpdateAccountsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/UpdateAccountsTask.java
deleted file mode 100644 (file)
index 667bf99..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * 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.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.LedgerAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-
-import java.util.ArrayList;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class UpdateAccountsTask extends AsyncTask<Void, Void, ArrayList<LedgerAccount>> {
-    protected ArrayList<LedgerAccount> doInBackground(Void... params) {
-        Data.backgroundTaskStarted();
-        try {
-            MobileLedgerProfile profile = Data.profile.getValue();
-            if (profile == null) throw new AssertionError();
-            String profileUUID = profile.getUuid();
-            boolean onlyStarred = Data.optShowOnlyStarred.get();
-            ArrayList<LedgerAccount> newList = new ArrayList<>();
-
-            String sql = "SELECT a.name from accounts a WHERE a.profile = ?";
-            if (onlyStarred) sql += " AND a.hidden = 0";
-            sql += " ORDER BY a.name";
-
-            SQLiteDatabase db = App.getDatabase();
-            try (Cursor cursor = db.rawQuery(sql, new String[]{profileUUID})) {
-                while (cursor.moveToNext()) {
-                    final String accName = cursor.getString(0);
-//                    debug("accounts",
-//                            String.format("Read account '%s' from DB [%s]", accName, profileUUID));
-                    LedgerAccount acc = profile.loadAccount(db, accName);
-                    if (acc.isVisible(newList)) newList.add(acc);
-                }
-            }
-
-            return newList;
-        }
-        finally {
-            debug("UAT", "decrementing background task count");
-            Data.backgroundTaskFinished();
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java b/app/src/main/java/net/ktnx/mobileledger/async/UpdateTransactionsTask.java
deleted file mode 100644 (file)
index 5093d50..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.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.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.Globals;
-
-import java.text.ParseException;
-import java.util.ArrayList;
-import java.util.Date;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class UpdateTransactionsTask extends AsyncTask<String, Void, String> {
-    protected String doInBackground(String[] filterAccName) {
-        final MobileLedgerProfile profile = Data.profile.getValue();
-        if (profile == null) return "Profile not configured";
-
-        String profile_uuid = profile.getUuid();
-        Data.backgroundTaskStarted();
-        try {
-            ArrayList<TransactionListItem> newList = new ArrayList<>();
-
-            String sql;
-            String[] params;
-
-            if (filterAccName[0] == null) {
-                sql = "SELECT id, date FROM transactions WHERE profile=? ORDER BY date desc, id " +
-                      "desc";
-                params = new String[]{profile_uuid};
-
-            }
-            else {
-                sql = "SELECT distinct tr.id, tr.date 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.date desc, tr.id desc";
-                params = new String[]{profile_uuid, filterAccName[0]};
-            }
-
-            debug("UTT", sql);
-            SQLiteDatabase db = App.getDatabase();
-            String lastDateString = Globals.formatLedgerDate(new Date());
-            Date lastDate = Globals.parseLedgerDate(lastDateString);
-            boolean odd = true;
-            try (Cursor cursor = db.rawQuery(sql, params)) {
-                while (cursor.moveToNext()) {
-                    if (isCancelled()) return null;
-
-                    int transaction_id = cursor.getInt(0);
-                    String dateString = cursor.getString(1);
-                    Date date = Globals.parseLedgerDate(dateString);
-
-                    if (!lastDateString.equals(dateString)) {
-                        boolean showMonth = (date.getMonth() != lastDate.getMonth() ||
-                                             date.getYear() != lastDate.getYear());
-                        newList.add(new TransactionListItem(date, showMonth));
-                    }
-                    newList.add(
-                            new TransactionListItem(new LedgerTransaction(transaction_id), odd));
-//                    debug("UTT", String.format("got transaction %d", transaction_id));
-
-                    lastDate = date;
-                    lastDateString = dateString;
-                    odd = !odd;
-                }
-                Data.transactions.setList(newList);
-                debug("UTT", "transaction list value updated");
-            }
-
-            return null;
-        }
-        catch (ParseException e) {
-            return String.format("Error parsing stored date '%s'", e.getMessage());
-        }
-        finally {
-            Data.backgroundTaskFinished();
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigIO.java
new file mode 100644 (file)
index 0000000..ec7a949
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/ConfigReader.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigReader.java
new file mode 100644 (file)
index 0000000..187eb9b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java b/app/src/main/java/net/ktnx/mobileledger/backup/ConfigWriter.java
new file mode 100644 (file)
index 0000000..b1a2814
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java b/app/src/main/java/net/ktnx/mobileledger/backup/MobileLedgerBackupAgent.java
new file mode 100644 (file)
index 0000000..6653843
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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");
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java b/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigReader.java
new file mode 100644 (file)
index 0000000..95b27b3
--- /dev/null
@@ -0,0 +1,401 @@
+/*
+ * 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");
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java b/app/src/main/java/net/ktnx/mobileledger/backup/RawConfigWriter.java
new file mode 100644 (file)
index 0000000..751a74a
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * 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());
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/AccountDAO.java
new file mode 100644 (file)
index 0000000..5ad4c90
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/AccountValueDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/AccountValueDAO.java
new file mode 100644 (file)
index 0000000..8a7e5fb
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/AsyncResultCallback.java b/app/src/main/java/net/ktnx/mobileledger/dao/AsyncResultCallback.java
new file mode 100644 (file)
index 0000000..df607ae
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/BaseDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/BaseDAO.java
new file mode 100644 (file)
index 0000000..905bf23
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/CurrencyDAO.java
new file mode 100644 (file)
index 0000000..1505a8f
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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();
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/OptionDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/OptionDAO.java
new file mode 100644 (file)
index 0000000..41d6464
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/ProfileDAO.java
new file mode 100644 (file)
index 0000000..c7a2e5d
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * 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();
+
+        });
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TemplateAccountDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TemplateAccountDAO.java
new file mode 100644 (file)
index 0000000..c2be8c7
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TemplateHeaderDAO.java
new file mode 100644 (file)
index 0000000..67dcef8
--- /dev/null
@@ -0,0 +1,158 @@
+/*
+ * 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));
+        });
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionAccountDAO.java
new file mode 100644 (file)
index 0000000..1b6f27d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java b/app/src/main/java/net/ktnx/mobileledger/dao/TransactionDAO.java
new file mode 100644 (file)
index 0000000..c398dbd
--- /dev/null
@@ -0,0 +1,298 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Account.java b/app/src/main/java/net/ktnx/mobileledger/db/Account.java
new file mode 100644 (file)
index 0000000..82524fc
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java b/app/src/main/java/net/ktnx/mobileledger/db/AccountAutocompleteAdapter.java
new file mode 100644 (file)
index 0000000..6a92b61
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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();
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/AccountValue.java b/app/src/main/java/net/ktnx/mobileledger/db/AccountValue.java
new file mode 100644 (file)
index 0000000..2ebc3ec
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmounts.java b/app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmounts.java
new file mode 100644 (file)
index 0000000..86742ca
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmountsAutocompleteAdapter.java b/app/src/main/java/net/ktnx/mobileledger/db/AccountWithAmountsAutocompleteAdapter.java
new file mode 100644 (file)
index 0000000..e99df9f
--- /dev/null
@@ -0,0 +1,109 @@
+/*
+ * 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();
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Currency.java b/app/src/main/java/net/ktnx/mobileledger/db/Currency.java
new file mode 100644 (file)
index 0000000..8552443
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/DB.java b/app/src/main/java/net/ktnx/mobileledger/db/DB.java
new file mode 100644 (file)
index 0000000..7d3e780
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Option.java b/app/src/main/java/net/ktnx/mobileledger/db/Option.java
new file mode 100644 (file)
index 0000000..8fabb55
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Profile.java b/app/src/main/java/net/ktnx/mobileledger/db/Profile.java
new file mode 100644 (file)
index 0000000..35230e2
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * 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);
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateAccount.java
new file mode 100644 (file)
index 0000000..949bd9d
--- /dev/null
@@ -0,0 +1,175 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateBase.java
new file mode 100644 (file)
index 0000000..83a963a
--- /dev/null
@@ -0,0 +1,20 @@
+/*
+ * 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 {}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateHeader.java
new file mode 100644 (file)
index 0000000..994c330
--- /dev/null
@@ -0,0 +1,227 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java b/app/src/main/java/net/ktnx/mobileledger/db/TemplateWithAccounts.java
new file mode 100644 (file)
index 0000000..a96236e
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java b/app/src/main/java/net/ktnx/mobileledger/db/Transaction.java
new file mode 100644 (file)
index 0000000..8b98f92
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java b/app/src/main/java/net/ktnx/mobileledger/db/TransactionAccount.java
new file mode 100644 (file)
index 0000000..d6417cd
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TransactionDescriptionAutocompleteAdapter.java b/app/src/main/java/net/ktnx/mobileledger/db/TransactionDescriptionAutocompleteAdapter.java
new file mode 100644 (file)
index 0000000..be4b0a8
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * 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();
+            }
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/db/TransactionWithAccounts.java b/app/src/main/java/net/ktnx/mobileledger/db/TransactionWithAccounts.java
new file mode 100644 (file)
index 0000000..5e91e4e
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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;
+}
index 17b578c4987ef644435e47413591b36d792daf1e..7d281650caa41f8d7f57817c1f2dc3c1a74783e8 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.err;
 
-public class HTTPException extends Throwable {
+public class HTTPException extends Exception {
     private final int responseCode;
     private final int responseCode;
-    private final String responseMessage;
     public int getResponseCode() {
         return responseCode;
     }
     public int getResponseCode() {
         return responseCode;
     }
-    public String getResponseMessage() {
-        return responseMessage;
-    }
     public HTTPException(int responseCode, String responseMessage) {
     public HTTPException(int responseCode, String responseMessage) {
+        super(responseMessage);
         this.responseCode = responseCode;
         this.responseCode = responseCode;
-        this.responseMessage = responseMessage;
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/API.java b/app/src/main/java/net/ktnx/mobileledger/json/API.java
new file mode 100644 (file)
index 0000000..136bdf5
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/AccountListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/AccountListParser.java
new file mode 100644 (file)
index 0000000..baeeb2e
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * 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;
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ApiNotSupportedException.java b/app/src/main/java/net/ktnx/mobileledger/json/ApiNotSupportedException.java
new file mode 100644 (file)
index 0000000..7bf8451
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/Gateway.java b/app/src/main/java/net/ktnx/mobileledger/json/Gateway.java
new file mode 100644 (file)
index 0000000..521e4c9
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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;
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedBalance.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedBalance.java
new file mode 100644 (file)
index 0000000..49ee26d
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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;
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedLedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedLedgerAccount.java
new file mode 100644 (file)
index 0000000..e0b0d34
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedPosting.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedPosting.java
new file mode 100644 (file)
index 0000000..75a98d2
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * 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.Currency;
+import net.ktnx.mobileledger.model.Data;
+
+public class ParsedPosting {
+    protected static boolean getCommoditySpaced() {
+        return Data.currencyGap.getValue();
+    }
+    protected static char getCommoditySide() {
+        return (Data.currencySymbolPosition.getValue() == Currency.Position.after) ? 'R' : 'L';
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedPrice.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedPrice.java
new file mode 100644 (file)
index 0000000..3959d14
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedQuantity.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedQuantity.java
new file mode 100644 (file)
index 0000000..2068479
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/ParsedStyle.java b/app/src/main/java/net/ktnx/mobileledger/json/ParsedStyle.java
new file mode 100644 (file)
index 0000000..c22cd3f
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/TransactionListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/TransactionListParser.java
new file mode 100644 (file)
index 0000000..20bf08b
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+}
index e3f0e384b4da23d6a20f1fd2944875b5d97c2162..0c589520d4904a27ce1cfce2ca238e496d44c22f 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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 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 java.io.IOException;
 import java.io.InputStream;
 
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class AccountListParser {
-
-    private final MappingIterator<ParsedLedgerAccount> iter;
+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);
 
 
     public AccountListParser(InputStream input) throws IOException {
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerAccount.class);
 
-        iter = reader.readValues(input);
+        iterator = reader.readValues(input);
     }
     }
-    public ParsedLedgerAccount nextAccount() {
-        if (!iter.hasNext()) return null;
-
-        ParsedLedgerAccount next = iter.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;
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_14/Gateway.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_14/Gateway.java
new file mode 100644 (file)
index 0000000..25712af
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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);
+    }
+}
index 39cd1908ff308f1f450b9453c577d98180e38835..cb8b5f5fbe3bdf9ab225e8f258e326bcc785c0dd 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.json.v1_14;
 
-import androidx.annotation.NonNull;
-
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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() {
     }
     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;
     }
     public ParsedStyle getAstyle() {
         return astyle;
     }
index d346cb76009c3d50964a5937576f9960abc2eba0..476e9cd713936c8c3474151b5b2e00216ac87424 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,32 +19,35 @@ package net.ktnx.mobileledger.json.v1_14;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
+import java.util.ArrayList;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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 List<ParsedBalance> aebalance;
     private List<ParsedBalance> aibalance;
-    private String aname;
     public ParsedLedgerAccount() {
     }
     public List<ParsedBalance> getAebalance() {
         return aebalance;
     }
     public ParsedLedgerAccount() {
     }
     public List<ParsedBalance> getAebalance() {
         return aebalance;
     }
-    public List<ParsedBalance> getAibalance() {
-        return aibalance;
-    }
     public void setAebalance(List<ParsedBalance> aebalance) {
         this.aebalance = 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 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;
+    }
 }
 }
index e95c27c17aee7a85373bcef949ababf09fecac81..6c8841595e15989922ff6b74beafef2c598f6775 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -22,11 +22,11 @@ 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.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.text.ParseException;
 import java.util.ArrayList;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -48,7 +48,7 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
     public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
         ParsedLedgerTransaction
                 result = new ParsedLedgerTransaction();
     public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
         ParsedLedgerTransaction
                 result = new ParsedLedgerTransaction();
-        result.setTcomment("");
+        result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
         result.setTprecedingcomment("");
 
         ArrayList<ParsedPosting> postings = new ArrayList<>();
         result.setTprecedingcomment("");
 
         ArrayList<ParsedPosting> postings = new ArrayList<>();
@@ -59,9 +59,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         }
 
         result.setTpostings(postings);
         }
 
         result.setTpostings(postings);
-        Date transactionDate = tr.getDate();
+        SimpleDate transactionDate = tr.getDateIfAny();
         if (transactionDate == null) {
         if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
+            transactionDate = SimpleDate.today();
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
@@ -144,8 +144,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
-        Date date = Globals.parseIsoDate(tdate);
+        SimpleDate date = Globals.parseIsoDate(tdate);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
+        tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
 
         List<ParsedPosting> postings = tpostings;
 
 
         List<ParsedPosting> postings = tpostings;
 
index 21cc6dbff22130980ca02e624d2c01ef0d13efbb..472fee7c91864aabbcd58b947a4a12383e63eb33 100644 (file)
@@ -25,7 +25,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedPosting {
+public class ParsedPosting extends net.ktnx.mobileledger.json.ParsedPosting {
     private Void pbalanceassertion;
     private String pstatus = "Unmarked";
     private String paccount;
     private Void pbalanceassertion;
     private String pstatus = "Unmarked";
     private String paccount;
@@ -42,6 +42,12 @@ public class ParsedPosting {
     public static ParsedPosting fromLedgerAccount(LedgerTransactionAccount acc) {
         ParsedPosting result = new ParsedPosting();
         result.setPaccount(acc.getAccountName());
     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());
         ArrayList<ParsedAmount> amounts = new ArrayList<>();
         ParsedAmount amt = new ParsedAmount();
         amt.setAcommodity((acc.getCurrency() == null) ? "" : acc.getCurrency());
@@ -51,11 +57,13 @@ public class ParsedPosting {
         qty.setDecimalMantissa(Math.round(acc.getAmount() * 100));
         amt.setAquantity(qty);
         ParsedStyle style = new ParsedStyle();
         qty.setDecimalMantissa(Math.round(acc.getAmount() * 100));
         amt.setAquantity(qty);
         ParsedStyle style = new ParsedStyle();
-        style.setAscommodityside('L');
-        style.setAscommodityspaced(false);
+        style.setAscommodityside(getCommoditySide());
+        style.setAscommodityspaced(getCommoditySpaced());
         style.setAsprecision(2);
         style.setAsdecimalpoint('.');
         amt.setAstyle(style);
         style.setAsprecision(2);
         style.setAsdecimalpoint('.');
         amt.setAstyle(style);
+        if (acc.getCurrency() != null)
+            amt.setAcommodity(acc.getCurrency());
         amounts.add(amt);
         result.setPamount(amounts);
         return result;
         amounts.add(amt);
         result.setPamount(amounts);
         return result;
@@ -88,7 +96,7 @@ public class ParsedPosting {
         return pcomment;
     }
     public void setPcomment(String pcomment) {
         return pcomment;
     }
     public void setPcomment(String pcomment) {
-        this.pcomment = pcomment;
+        this.pcomment = (pcomment == null) ? null : pcomment.trim();
     }
     public List<List<String>> getPtags() {
         return ptags;
     }
     public List<List<String>> getPtags() {
         return ptags;
@@ -129,7 +137,8 @@ public class ParsedPosting {
     public LedgerTransactionAccount asLedgerAccount() {
         ParsedAmount amt = pamount.get(0);
         return new LedgerTransactionAccount(paccount, amt.getAquantity()
     public LedgerTransactionAccount asLedgerAccount() {
         ParsedAmount amt = pamount.get(0);
         return new LedgerTransactionAccount(paccount, amt.getAquantity()
-                                                         .asFloat(), amt.getAcommodity());
+                                                         .asFloat(), amt.getAcommodity(),
+                getPcomment());
     }
 
 }
     }
 
 }
index 7f457a7c35a10474c5d0b28bc434431f997812d0..b00936ec2b2838de2d2f3d9322cdccafc73c3e09 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -35,7 +35,7 @@ class ParsedPrice {
     public void setTag(String tag) {
         this.tag = tag;
     }
     public void setTag(String tag) {
         this.tag = tag;
     }
-    private class Contents {
+    private static class Contents {
         private ParsedPrice aprice;
         private ParsedQuantity aquantity;
         private String acommodity;
         private ParsedPrice aprice;
         private ParsedQuantity aquantity;
         private String acommodity;
index 72b6fc612165a65cad9521054e57492432b6e900..5c27f6fbab8fe10e05075da1ed5aad1f062f6d01 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -47,11 +47,11 @@ public class ParsedQuantity {
         int pointPos = input.indexOf('.');
         if (pointPos >= 0) {
             String integral = input.replace(".", "");
         int pointPos = input.indexOf('.');
         if (pointPos >= 0) {
             String integral = input.replace(".", "");
-            decimalMantissa = Long.valueOf(integral);
+            decimalMantissa = Long.parseLong(integral);
             decimalPlaces = input.length() - pointPos - 1;
         }
         else {
             decimalPlaces = input.length() - pointPos - 1;
         }
         else {
-            decimalMantissa = Long.valueOf(input);
+            decimalMantissa = Long.parseLong(input);
             decimalPlaces = 0;
         }
     }
             decimalPlaces = 0;
         }
     }
index 798cb81c8bff5d03ff4b9b489bee2e236f2f90bb..74b02a5090de22278eaafaec1dc70b42e1ad9974 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -20,12 +20,8 @@ package net.ktnx.mobileledger.json.v1_14;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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 int asprecision;
-    private char asdecimalpoint;
-    private char ascommodityside;
-    private int digitgroups;
-    private boolean ascommodityspaced;
     public ParsedStyle() {
     }
     public int getAsprecision() {
     public ParsedStyle() {
     }
     public int getAsprecision() {
@@ -34,28 +30,4 @@ public class ParsedStyle {
     public void setAsprecision(int asprecision) {
         this.asprecision = 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;
-    }
 }
 }
index 805668507a4c2065aad1c676904d494818190e5b..f9be5a3ae4fda936ee40675b424736fe79180cbf 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -21,20 +21,24 @@ import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 
 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.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> iter;
+    private final MappingIterator<ParsedLedgerTransaction> iterator;
 
     public TransactionListParser(InputStream input) throws IOException {
 
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
 
     public TransactionListParser(InputStream input) throws IOException {
 
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
-        iter = reader.readValues(input);
+        iterator = reader.readValues(input);
     }
     }
-    public ParsedLedgerTransaction nextTransaction() {
-        return iter.hasNext() ? iter.next() : null;
+    public LedgerTransaction nextTransaction() throws ParseException {
+        return iterator.hasNext() ? iterator.next()
+                                            .asLedgerTransaction() : null;
     }
 }
     }
 }
index 4206d0fc482429a475fbbb5628c5a834e86a836b..f8f4dbb3d417669d17ba611daea6930a0648dfa6 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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_15;
 
 
 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 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 java.io.IOException;
 import java.io.InputStream;
 
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class AccountListParser {
-
-    private final MappingIterator<ParsedLedgerAccount> iter;
+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);
 
 
     public AccountListParser(InputStream input) throws IOException {
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerAccount.class);
 
-        iter = reader.readValues(input);
+        iterator = reader.readValues(input);
     }
     }
-    public ParsedLedgerAccount nextAccount() {
-        if (!iter.hasNext()) return null;
-
-        ParsedLedgerAccount next = iter.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_15;
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_15/Gateway.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_15/Gateway.java
new file mode 100644 (file)
index 0000000..a4a1268
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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);
+    }
+}
index 24e9ccb07f8015523c287e4860d00d359dff5d0c..371c12210411d9d2d5e7e6ca0e0f8c2902c5b217 100644 (file)
 
 package net.ktnx.mobileledger.json.v1_15;
 
 
 package net.ktnx.mobileledger.json.v1_15;
 
-import androidx.annotation.NonNull;
-
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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() {
     }
     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;
     }
     public ParsedStyle getAstyle() {
         return astyle;
     }
index 89f3cccefc67abb88446c1477fca130ae86de571..8e762cabe8a5060969d1f12625bbd8420da0a36b 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,32 +19,5 @@ package net.ktnx.mobileledger.json.v1_15;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
-import java.util.List;
-
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedLedgerAccount {
-    private List<ParsedBalance> aebalance;
-    private List<ParsedBalance> aibalance;
-    private String aname;
-    public ParsedLedgerAccount() {
-    }
-    public List<ParsedBalance> getAebalance() {
-        return aebalance;
-    }
-    public List<ParsedBalance> getAibalance() {
-        return aibalance;
-    }
-    public void setAebalance(List<ParsedBalance> aebalance) {
-        this.aebalance = aebalance;
-    }
-    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 {}
index 2f6867dcff4f75f490fb8c0f7988df4b3c3198b7..bc1950badca0ff381f701c0cb511fe3d36da7da4 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -22,11 +22,11 @@ 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.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.text.ParseException;
 import java.util.ArrayList;
-import java.util.Date;
-import java.util.GregorianCalendar;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -46,7 +46,7 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
     }
     public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
         ParsedLedgerTransaction result = new ParsedLedgerTransaction();
     }
     public static ParsedLedgerTransaction fromLedgerTransaction(LedgerTransaction tr) {
         ParsedLedgerTransaction result = new ParsedLedgerTransaction();
-        result.setTcomment("");
+        result.setTcomment(Misc.nullIsEmpty(tr.getComment()));
         result.setTprecedingcomment("");
 
         ArrayList<ParsedPosting> postings = new ArrayList<>();
         result.setTprecedingcomment("");
 
         ArrayList<ParsedPosting> postings = new ArrayList<>();
@@ -57,9 +57,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         }
 
         result.setTpostings(postings);
         }
 
         result.setTpostings(postings);
-        Date transactionDate = tr.getDate();
+        SimpleDate transactionDate = tr.getDateIfAny();
         if (transactionDate == null) {
         if (transactionDate == null) {
-            transactionDate = new GregorianCalendar().getTime();
+            transactionDate = SimpleDate.today();
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
         }
         result.setTdate(Globals.formatIsoDate(transactionDate));
         result.setTdate2(null);
@@ -142,8 +142,9 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
         tpostings.add(posting);
     }
     public LedgerTransaction asLedgerTransaction() throws ParseException {
-        Date date = Globals.parseIsoDate(tdate);
+        SimpleDate date = Globals.parseIsoDate(tdate);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
         LedgerTransaction tr = new LedgerTransaction(tindex, date, tdescription);
+        tr.setComment(Misc.trim(Misc.emptyIsNull(tcomment)));
 
         List<ParsedPosting> postings = tpostings;
 
 
         List<ParsedPosting> postings = tpostings;
 
@@ -152,6 +153,8 @@ public class ParsedLedgerTransaction implements net.ktnx.mobileledger.json.Parse
                 tr.addAccount(p.asLedgerAccount());
             }
         }
                 tr.addAccount(p.asLedgerAccount());
             }
         }
+
+        tr.markDataAsLoaded();
         return tr;
     }
 }
         return tr;
     }
 }
index 7c20a7ba87a62ab669557978d37b50a236da6600..38777684bbd8e3aec545f407bb836d9945fce809 100644 (file)
@@ -25,32 +25,61 @@ import java.util.ArrayList;
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 import java.util.List;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
-public class ParsedPosting {
+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 Void pbalanceassertion;
     private String pstatus = "Unmarked";
     private String paccount;
     private List<ParsedAmount> pamount;
     private String pdate = null;
-    public String getPdate2() {
-        return pdate2;
-    }
-    public void setPdate2(String pdate2) {
-        this.pdate2 = pdate2;
-    }
     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_;
     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 int getPtransaction_() {
         return ptransaction_;
     }
     public void setPtransaction_(int ptransaction_) {
         this.ptransaction_ = ptransaction_;
     }
-    public ParsedPosting() {
-    }
     public String getPdate() {
         return pdate;
     }
     public String getPdate() {
         return pdate;
     }
@@ -67,7 +96,7 @@ public class ParsedPosting {
         return pcomment;
     }
     public void setPcomment(String pcomment) {
         return pcomment;
     }
     public void setPcomment(String pcomment) {
-        this.pcomment = pcomment;
+        this.pcomment = (pcomment == null) ? null : pcomment.trim();
     }
     public List<List<String>> getPtags() {
         return ptags;
     }
     public List<List<String>> getPtags() {
         return ptags;
@@ -107,29 +136,9 @@ public class ParsedPosting {
     }
     public LedgerTransactionAccount asLedgerAccount() {
         ParsedAmount amt = pamount.get(0);
     }
     public LedgerTransactionAccount asLedgerAccount() {
         ParsedAmount amt = pamount.get(0);
-        return new LedgerTransactionAccount(paccount, amt.getAquantity().asFloat(),
-                amt.getAcommodity());
-    }
-    public static ParsedPosting fromLedgerAccount(LedgerTransactionAccount acc) {
-        ParsedPosting result = new ParsedPosting();
-        result.setPaccount(acc.getAccountName());
-        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('L');
-        style.setAscommodityspaced(false);
-        style.setAsprecision(2);
-        style.setAsdecimalpoint('.');
-        amt.setAstyle(style);
-        amounts.add(amt);
-        result.setPamount(amounts);
-        return result;
+        return new LedgerTransactionAccount(paccount, amt.getAquantity()
+                                                         .asFloat(), amt.getAcommodity(),
+                getPcomment());
     }
 
 }
     }
 
 }
index 409ed8b259ceb94851caf4f978a3000235ee3fb5..0b93e15f46311e4b642941b000af18365b60e488 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -35,7 +35,7 @@ class ParsedPrice {
     public void setTag(String tag) {
         this.tag = tag;
     }
     public void setTag(String tag) {
         this.tag = tag;
     }
-    private class Contents {
+    private static class Contents {
         private ParsedPrice aprice;
         private ParsedQuantity aquantity;
         private String acommodity;
         private ParsedPrice aprice;
         private ParsedQuantity aquantity;
         private String acommodity;
index 898d59741a22cf53e774a8b9f15f977f5ce4371f..9a81fcc7170ea08bf5d61c35d84432d8a5fc6702 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -20,39 +20,5 @@ package net.ktnx.mobileledger.json.v1_15;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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.valueOf(integral);
-            decimalPlaces = input.length() - pointPos - 1;
-        }
-        else {
-            decimalMantissa = Long.valueOf(input);
-            decimalPlaces = 0;
-        }
-    }
+public class ParsedQuantity extends net.ktnx.mobileledger.json.ParsedQuantity {
 }
 }
index 554133c5105c72f18fcd9abe89e9218a59cbfa0d..b2f0f742acfb4ff394fede8ae7ad6003e83ca14a 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -20,42 +20,4 @@ package net.ktnx.mobileledger.json.v1_15;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 @JsonIgnoreProperties(ignoreUnknown = true)
 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 {}
index c6210746aa138e6e46d3a3d51167271374345d92..aab1bc41de499197e3d10f227583672825a78189 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -21,20 +21,24 @@ import com.fasterxml.jackson.databind.MappingIterator;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectReader;
 
 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.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> iter;
+    private final MappingIterator<ParsedLedgerTransaction> iterator;
 
     public TransactionListParser(InputStream input) throws IOException {
 
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
 
     public TransactionListParser(InputStream input) throws IOException {
 
         ObjectMapper mapper = new ObjectMapper();
         ObjectReader reader = mapper.readerFor(ParsedLedgerTransaction.class);
-        iter = reader.readValues(input);
+        iterator = reader.readValues(input);
     }
     }
-    public ParsedLedgerTransaction nextTransaction() {
-        return iter.hasNext() ? iter.next() : null;
+    public LedgerTransaction nextTransaction() throws ParseException {
+        return iterator.hasNext() ? iterator.next()
+                                            .asLedgerTransaction() : null;
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/AccountListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/AccountListParser.java
new file mode 100644 (file)
index 0000000..85a64c8
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/Gateway.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/Gateway.java
new file mode 100644 (file)
index 0000000..239ec2d
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedAmount.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedAmount.java
new file mode 100644 (file)
index 0000000..12cdbac
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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;
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedBalance.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedBalance.java
new file mode 100644 (file)
index 0000000..5defe08
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerAccount.java
new file mode 100644 (file)
index 0000000..5fabdb5
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedLedgerTransaction.java
new file mode 100644 (file)
index 0000000..196408c
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPosting.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPosting.java
new file mode 100644 (file)
index 0000000..e233062
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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());
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrecision.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrecision.java
new file mode 100644 (file)
index 0000000..4472cf4
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrice.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedPrice.java
new file mode 100644 (file)
index 0000000..0e1ea54
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedQuantity.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedQuantity.java
new file mode 100644 (file)
index 0000000..951bb56
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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 {}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedSourcePos.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedSourcePos.java
new file mode 100644 (file)
index 0000000..650f97c
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedStyle.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/ParsedStyle.java
new file mode 100644 (file)
index 0000000..b0398a1
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/TransactionListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_19_1/TransactionListParser.java
new file mode 100644 (file)
index 0000000..12564a2
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/AccountListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/AccountListParser.java
new file mode 100644 (file)
index 0000000..904fb57
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/Gateway.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/Gateway.java
new file mode 100644 (file)
index 0000000..c04f6f2
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedAmount.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedAmount.java
new file mode 100644 (file)
index 0000000..58f88c1
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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;
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedBalance.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedBalance.java
new file mode 100644 (file)
index 0000000..4f74de4
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerAccount.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerAccount.java
new file mode 100644 (file)
index 0000000..6d942e8
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerTransaction.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedLedgerTransaction.java
new file mode 100644 (file)
index 0000000..59d1763
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPosting.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPosting.java
new file mode 100644 (file)
index 0000000..e60bd19
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * 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());
+    }
+
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPrice.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedPrice.java
new file mode 100644 (file)
index 0000000..80a810a
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * 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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedQuantity.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedQuantity.java
new file mode 100644 (file)
index 0000000..5502693
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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 {}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedSourcePos.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedSourcePos.java
new file mode 100644 (file)
index 0000000..b3ea5db
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedStyle.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/ParsedStyle.java
new file mode 100644 (file)
index 0000000..d3a0a13
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/json/v1_23/TransactionListParser.java b/app/src/main/java/net/ktnx/mobileledger/json/v1_23/TransactionListParser.java
new file mode 100644 (file)
index 0000000..7f5350a
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java b/app/src/main/java/net/ktnx/mobileledger/model/AccountListItem.java
new file mode 100644 (file)
index 0000000..807e93d
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * 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.model;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+
+import org.jetbrains.annotations.NotNull;
+
+public abstract class AccountListItem {
+    private AccountListItem() {}
+    public abstract boolean sameContent(AccountListItem other);
+    @NonNull
+    public Type getType() {
+        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;
+    }
+    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;
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/Currency.java b/app/src/main/java/net/ktnx/mobileledger/model/Currency.java
new file mode 100644 (file)
index 0000000..bfe84ea
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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 net.ktnx.mobileledger.utils.Misc;
+
+public class Currency {
+    private final int id;
+    private String name;
+    private Position position;
+    private boolean hasGap;
+    public Currency(int id, String name) {
+        this.id = id;
+        this.name = name;
+        position = Position.after;
+        hasGap = true;
+    }
+    public Currency(int id, String name, Position position, boolean hasGap) {
+        this.id = id;
+        this.name = name;
+        this.position = position;
+        this.hasGap = hasGap;
+    }
+    static public boolean equal(Currency left, Currency right) {
+        if (left == null) {
+            return right == null;
+        }
+        else
+            return left.equals(right);
+    }
+    static public boolean equal(Currency left, String right) {
+        right = Misc.emptyIsNull(right);
+        if (left == null) {
+            return right == null;
+        }
+        else {
+            String leftName = Misc.emptyIsNull(left.getName());
+            if (leftName == null) {
+                return right == null;
+            }
+            else
+                return leftName.equals(right);
+        }
+    }
+    public int getId() {
+        return id;
+    }
+    public String getName() {
+        return name;
+    }
+    public void setName(String name) {
+        this.name = name;
+    }
+    public Position getPosition() {
+        return position;
+    }
+    public void setPosition(Position position) {
+        this.position = position;
+    }
+    public boolean hasGap() {
+        return hasGap;
+    }
+    public void setHasGap(boolean hasGap) {
+        this.hasGap = hasGap;
+    }
+    public enum Position {
+        before, after, unknown, none
+    }
+}
index 223a61e8aa7ebe621a2df2b3b051c2d589a31378..91a3e6d663881474bbe6231ad0564b063226e380 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.model;
 
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
+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.async.RetrieveTransactionsTask;
-import net.ktnx.mobileledger.ui.activity.MainActivity;
-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.Locker;
 import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.ObservableList;
-import net.ktnx.mobileledger.utils.ObservableValue;
 
 
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
+import org.jetbrains.annotations.NotNull;
+
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import androidx.lifecycle.MutableLiveData;
-
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public final class Data {
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public final class Data {
-    public static ObservableList<TransactionListItem> transactions =
-            new ObservableList<>(new ArrayList<>());
-    public static ObservableList<LedgerAccount> accounts = new ObservableList<>(new ArrayList<>());
-    public static MutableLiveData<Boolean> backgroundTasksRunning = new MutableLiveData<>(false);
-    public static MutableLiveData<Date> lastUpdateDate = new MutableLiveData<>();
-    public static MutableLiveData<MobileLedgerProfile> profile = new MutableLiveData<>();
-    public static MutableLiveData<ArrayList<MobileLedgerProfile>> profiles =
-            new MutableLiveData<>(null);
-    public static ObservableValue<Boolean> optShowOnlyStarred = new ObservableValue<>();
-    public static MutableLiveData<String> accountFilter = new MutableLiveData<>();
-    private static AtomicInteger backgroundTaskCount = new AtomicInteger(0);
-    private static Locker profilesLocker = new Locker();
-    private static RetrieveTransactionsTask retrieveTransactionsTask;
+    public static final MutableLiveData<Boolean> backgroundTasksRunning =
+            new MutableLiveData<>(false);
+    public static final MutableLiveData<RetrieveTransactionsTask.Progress> backgroundTaskProgress =
+            new MutableLiveData<>();
+    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<Locale> locale = new MutableLiveData<>();
+    public static final MutableLiveData<Boolean> drawerOpen = new MutableLiveData<>(false);
+    public static final MutableLiveData<Date> lastUpdateDate = new MutableLiveData<>(null);
+    public static final MutableLiveData<Integer> lastUpdateTransactionCount =
+            new MutableLiveData<>(0);
+    public static final MutableLiveData<Integer> lastUpdateAccountCount = new MutableLiveData<>(0);
+    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());
+    }
+
+    public static String getDecimalSeparator() {
+        return decimalSeparator;
+    }
+    @Nullable
+    public static Profile getProfile() {
+        return profile.getValue();
+    }
     public static void backgroundTaskStarted() {
         int cnt = backgroundTaskCount.incrementAndGet();
         debug("data",
     public static void backgroundTaskStarted() {
         int cnt = backgroundTaskCount.incrementAndGet();
         debug("data",
@@ -70,85 +95,71 @@ public final class Data {
                         cnt));
         backgroundTasksRunning.postValue(cnt > 0);
     }
                         cnt));
         backgroundTasksRunning.postValue(cnt > 0);
     }
-    public static void setCurrentProfile(MobileLedgerProfile newProfile) {
-        MLDB.setOption(MLDB.OPT_PROFILE_UUID, (newProfile == null) ? null : newProfile.getUuid());
-        stopTransactionsRetrieval();
-        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;
-        }
+    public static void setCurrentProfile(Profile newProfile) {
+        profile.setValue(newProfile);
     }
     }
-    @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 void postCurrentProfile(Profile newProfile) {
+        profile.postValue(newProfile);
     }
     }
-    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);
-        }
+    public static void refreshCurrencyData(Locale locale) {
+        NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
+        java.util.Currency currency = formatter.getCurrency();
+        String symbol = currency != null ? currency.getSymbol() : "";
+        Logger.debug("locale", String.format(
+                "Discovering currency symbol position for locale %s (currency is %s; symbol is %s)",
+                locale.toString(), currency != null ? currency.toString() : "<none>", symbol));
+        String formatted = formatter.format(1234.56f);
+        Logger.debug("locale", String.format("1234.56 formats as '%s'", formatted));
 
 
-        return -1;
-    }
-    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);
-            }
+        if (formatted.startsWith(symbol)) {
+            currencySymbolPosition.setValue(Currency.Position.before);
+
+            // is the currency symbol directly followed by the first formatted digit?
+            final char canary = formatted.charAt(symbol.length());
+            currencyGap.setValue(canary != '1');
         }
         }
-        return profile;
-    }
-    public synchronized static void scheduleTransactionListRetrieval(MainActivity activity) {
-        if (retrieveTransactionsTask != null) {
-            Logger.debug("db", "Ignoring request for transaction retrieval - already active");
-            return;
+        else if (formatted.endsWith(symbol)) {
+            currencySymbolPosition.setValue(Currency.Position.after);
+
+            // is the currency symbol directly preceded bu the last formatted digit?
+            final char canary = formatted.charAt(formatted.length() - symbol.length() - 1);
+            currencyGap.setValue(canary != '6');
         }
         }
-        MobileLedgerProfile pr = profile.getValue();
-        if (pr == null) throw new IllegalStateException("No current profile");
+        else
+            currencySymbolPosition.setValue(Currency.Position.none);
+
+        NumberFormat newNumberFormatter = NumberFormat.getNumberInstance();
+        newNumberFormatter.setParseIntegerOnly(false);
+        newNumberFormatter.setGroupingUsed(true);
+        newNumberFormatter.setMinimumIntegerDigits(1);
+        newNumberFormatter.setMinimumFractionDigits(2);
 
 
-        retrieveTransactionsTask =
-                new RetrieveTransactionsTask(new WeakReference<>(activity), profile.getValue());
-        Logger.debug("db", "Created a background transaction retrieval task");
+        numberFormatter = newNumberFormatter;
 
 
-        retrieveTransactionsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance(locale)
+                                                              .getMonetaryDecimalSeparator());
+    }
+    @NotNull
+    public static String formatCurrency(float number) {
+        NumberFormat formatter = NumberFormat.getCurrencyInstance(locale.getValue());
+        return formatter.format(number);
     }
     }
-    public static synchronized void stopTransactionsRetrieval() {
-        if (retrieveTransactionsTask != null) retrieveTransactionsTask.cancel(false);
+    @NotNull
+    public static String formatNumber(float number) {
+        return numberFormatter.format(number);
     }
     }
-    public static void transactionRetrievalDone() {
-        retrieveTransactionsTask = null;
+    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
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java b/app/src/main/java/net/ktnx/mobileledger/model/FutureDates.java
new file mode 100644 (file)
index 0000000..db80db1
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * 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);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java b/app/src/main/java/net/ktnx/mobileledger/model/HledgerVersion.java
new file mode 100644 (file)
index 0000000..235f8c7
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.ktnx.mobileledger.json.API;
+
+import java.util.Locale;
+
+public class HledgerVersion {
+    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.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_1 = false;
+        this.hasPatch = 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_1 = origin.isPre_1_20_1;
+        this.patch = origin.patch;
+        this.hasPatch = origin.hasPatch;
+    }
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null)
+            return false;
+        if (!(obj instanceof HledgerVersion))
+            return false;
+        HledgerVersion that = (HledgerVersion) obj;
+
+        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_1() {
+        return isPre_1_20_1;
+    }
+    public int getMajor() {
+        return major;
+    }
+    public int getMinor() {
+        return minor;
+    }
+    public int getPatch() {
+        return patch;
+    }
+    @NonNull
+    @Override
+    public String toString() {
+        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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/InertMutableLiveData.java b/app/src/main/java/net/ktnx/mobileledger/model/InertMutableLiveData.java
new file mode 100644 (file)
index 0000000..b7048ab
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * 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 androidx.lifecycle.MutableLiveData;
+
+public class InertMutableLiveData<T> extends MutableLiveData<T> {
+    public InertMutableLiveData(T initialValue) {
+        super(initialValue);
+    }
+    public InertMutableLiveData() {
+        super();
+    }
+    @Override
+    public void setValue(T value) {
+        if (getValue() != value)
+            super.setValue(value);
+    }
+}
index 256be6f082f7ee936f94943efde6782cf82386c3..c2e62772f5990ef911854c838218edde70e50f82 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.model;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+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.ArrayList;
 import java.util.List;
-import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.regex.Pattern;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
 public class LedgerAccount {
 public class LedgerAccount {
+    private static final char ACCOUNT_DELIMITER = ':';
     static Pattern reHigherAccount = Pattern.compile("^[^:]+:");
     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 String name;
     private String shortName;
     private int level;
-    private String parentName;
-    private boolean hiddenByStar;
-    private boolean hiddenByStarToBe;
     private boolean expanded;
     private List<LedgerAmount> amounts;
     private boolean hasSubAccounts;
     private boolean amountsExpanded;
 
     private boolean expanded;
     private List<LedgerAmount> amounts;
     private boolean hasSubAccounts;
     private boolean amountsExpanded;
 
-    public LedgerAccount(String name) {
+    public LedgerAccount(String name, @Nullable LedgerAccount parent) {
+        this.parent = parent;
+        if (parent != null && !name.startsWith(parent.getName() + ":"))
+            throw new IllegalStateException(
+                    String.format("Account name '%s' doesn't match parent account '%s'", name,
+                            parent.getName()));
         this.setName(name);
         this.setName(name);
-        hiddenByStar = false;
     }
     }
+    @Nullable
+    public static String extractParentName(@NonNull String accName) {
+        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 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()));
+        }
 
 
-    public LedgerAccount(String name, float amount) {
-        this.setName(name);
-        this.hiddenByStar = false;
-        this.expanded = true;
-        this.amounts = new ArrayList<LedgerAmount>();
-        this.addAmount(amount);
+        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() {
     }
     @Override
     public int hashCode() {
@@ -56,54 +92,37 @@ public class LedgerAccount {
     }
     @Override
     public boolean equals(@Nullable Object obj) {
     }
     @Override
     public boolean equals(@Nullable Object obj) {
-        if (obj == null) return false;
+        if (obj == null)
+            return false;
+
+        if (!(obj instanceof LedgerAccount))
+            return false;
 
 
-        return obj.getClass().equals(this.getClass()) &&
-               name.equals(((LedgerAccount) obj).getName());
+        LedgerAccount acc = (LedgerAccount) obj;
+        if (!name.equals(acc.name))
+            return false;
+
+        if (!getAmountsString().equals(acc.getAmountsString()))
+            return false;
+
+        return expanded == acc.expanded && amountsExpanded == acc.amountsExpanded;
     }
     // an account is visible if:
     }
     // an account is visible if:
-    //  - it is starred (not hidden by a star)
-    //  - and it has an expanded parent or is a top account
+    //  - it has an expanded visible parent or is a top account
     public boolean isVisible() {
     public boolean isVisible() {
-        if (hiddenByStar) return false;
-
-        if (level == 0) return true;
+        if (parent == null)
+            return true;
 
 
-        return isVisible(Data.accounts);
-    }
-    public boolean isVisible(List<LedgerAccount> list) {
-        for (LedgerAccount acc : list) {
-            if (acc.isParentOf(this)) {
-                if (!acc.isExpanded()) return false;
-            }
-        }
-        return true;
+        return (parent.isExpanded() && parent.isVisible());
     }
     public boolean isParentOf(LedgerAccount potentialChild) {
     }
     public boolean isParentOf(LedgerAccount potentialChild) {
-        return potentialChild.getName().startsWith(name + ":");
-    }
-    public boolean isHiddenByStar() {
-        return hiddenByStar;
-    }
-    public void setHiddenByStar(boolean hiddenByStar) {
-        this.hiddenByStar = hiddenByStar;
+        return potentialChild.getName()
+                             .startsWith(name + ":");
     }
     private void stripName() {
     }
     private void stripName() {
-        level = 0;
-        shortName = name;
-        StringBuilder parentBuilder = new StringBuilder();
-        while (true) {
-            Matcher m = reHigherAccount.matcher(shortName);
-            if (m.find()) {
-                level++;
-                parentBuilder.append(m.group(0));
-                shortName = m.replaceFirst("");
-            }
-            else break;
-        }
-        if (parentBuilder.length() > 0)
-            parentName = parentBuilder.substring(0, parentBuilder.length() - 1);
-        else parentName = null;
+        String[] split = name.split(":");
+        shortName = split[split.length - 1];
+        level = split.length - 1;
     }
     public String getName() {
         return name;
     }
     public String getName() {
         return name;
@@ -112,37 +131,43 @@ public class LedgerAccount {
         this.name = name;
         stripName();
     }
         this.name = name;
         stripName();
     }
-    public void addAmount(float amount, String currency) {
-        if (amounts == null) amounts = new ArrayList<>();
+    public void addAmount(float amount, @NonNull String currency) {
+        if (amounts == null)
+            amounts = new ArrayList<>();
         amounts.add(new LedgerAmount(amount, currency));
     }
     public void addAmount(float amount) {
         amounts.add(new LedgerAmount(amount, currency));
     }
     public void addAmount(float amount) {
-        this.addAmount(amount, null);
+        this.addAmount(amount, "");
     }
     public int getAmountCount() { return (amounts != null) ? amounts.size() : 0; }
     public String getAmountsString() {
     }
     public int getAmountCount() { return (amounts != null) ? amounts.size() : 0; }
     public String getAmountsString() {
-        if ((amounts == null) || amounts.isEmpty()) return "";
+        if ((amounts == null) || amounts.isEmpty())
+            return "";
 
         StringBuilder builder = new StringBuilder();
         for (LedgerAmount amount : amounts) {
             String amt = amount.toString();
 
         StringBuilder builder = new StringBuilder();
         for (LedgerAmount amount : amounts) {
             String amt = amount.toString();
-            if (builder.length() > 0) builder.append('\n');
+            if (builder.length() > 0)
+                builder.append('\n');
             builder.append(amt);
         }
 
         return builder.toString();
     }
     public String getAmountsString(int limit) {
             builder.append(amt);
         }
 
         return builder.toString();
     }
     public String getAmountsString(int limit) {
-        if ((amounts == null) || amounts.isEmpty()) return "";
+        if ((amounts == null) || amounts.isEmpty())
+            return "";
 
         int included = 0;
         StringBuilder builder = new StringBuilder();
         for (LedgerAmount amount : amounts) {
             String amt = amount.toString();
 
         int included = 0;
         StringBuilder builder = new StringBuilder();
         for (LedgerAmount amount : amounts) {
             String amt = amount.toString();
-            if (builder.length() > 0) builder.append('\n');
+            if (builder.length() > 0)
+                builder.append('\n');
             builder.append(amt);
             included++;
             builder.append(amt);
             included++;
-            if (included == limit) break;
+            if (included == limit)
+                break;
         }
 
         return builder.toString();
         }
 
         return builder.toString();
@@ -150,27 +175,12 @@ public class LedgerAccount {
     public int getLevel() {
         return level;
     }
     public int getLevel() {
         return level;
     }
-
     @NonNull
     public String getShortName() {
         return shortName;
     }
     @NonNull
     public String getShortName() {
         return shortName;
     }
-
     public String getParentName() {
     public String getParentName() {
-        return parentName;
-    }
-    public void togglehidden() {
-        hiddenByStar = !hiddenByStar;
-    }
-
-    public boolean isHiddenByStarToBe() {
-        return hiddenByStarToBe;
-    }
-    public void setHiddenByStarToBe(boolean hiddenByStarToBe) {
-        this.hiddenByStarToBe = hiddenByStarToBe;
-    }
-    public void toggleHiddenToBe() {
-        setHiddenByStarToBe(!hiddenByStarToBe);
+        return (parent == null) ? null : parent.getName();
     }
     public boolean hasSubAccounts() {
         return hasSubAccounts;
     }
     public boolean hasSubAccounts() {
         return hasSubAccounts;
@@ -188,10 +198,57 @@ public class LedgerAccount {
         expanded = !expanded;
     }
     public void removeAmounts() {
         expanded = !expanded;
     }
     public void removeAmounts() {
-        if (amounts != null) amounts.clear();
+        if (amounts != null)
+            amounts.clear();
+    }
+    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;
     }
     }
-    public boolean amountsExpanded() { return amountsExpanded; }
-    public void setAmountsExpanded(boolean flag) { amountsExpanded = flag; }
-    public void toggleAmountsExpanded() { amountsExpanded = !amountsExpanded; }
+    @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;
+    }
 }
 }
index 6aebdb50f7fb70dbf99cb8a5c43cf7b99cf21443..4e3cb8d21d34d4068dc007df6ab3da79bcaf0e4f 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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.annotation.SuppressLint;
 package net.ktnx.mobileledger.model;
 
 import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 
 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 {
 public class LedgerAmount {
-    private String currency;
-    private float amount;
+    private final String currency;
+    private final float amount;
+    private long dbId;
 
 
-    public
-    LedgerAmount(float amount, @NonNull String currency) {
+    public LedgerAmount(float amount, @NonNull String currency) {
         this.currency = currency;
         this.amount = amount;
     }
         this.currency = currency;
         this.amount = amount;
     }
-
-    public
-    LedgerAmount(float amount) {
+    public LedgerAmount(float amount) {
         this.amount = amount;
         this.currency = null;
     }
         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() {
     @SuppressLint("DefaultLocale")
     @NonNull
     public String toString() {
-        if (currency == null) return String.format("%,1.2f", amount);
-        else return String.format("%s %,1.2f", currency, amount);
+        if (currency == null)
+            return String.format("%,1.2f", amount);
+        else
+            return String.format("%s %,1.2f", currency, amount);
+    }
+    public void propagateToAccount(@NonNull LedgerAccount acc) {
+        if (currency != null)
+            acc.addAmount(amount, currency);
+        else
+            acc.addAmount(amount);
+    }
+    public String getCurrency() {
+        return currency;
+    }
+    public float getAmount() {
+        return amount;
     }
 }
     }
 }
index 2f23eff0a082a5d4c8866e498f99f348cf599337..cef83169f7a35ec21b304f08eb7c0b5b9dd25983 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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.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.Digest;
 import net.ktnx.mobileledger.utils.Globals;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 
-import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.Date;
+import java.util.List;
 
 public class LedgerTransaction {
     private static final String DIGEST_TYPE = "SHA-256";
 
 public class LedgerTransaction {
     private static final String DIGEST_TYPE = "SHA-256";
-    public final Comparator<LedgerTransactionAccount> comparator =
-            new Comparator<LedgerTransactionAccount>() {
-                @Override
-                public int compare(LedgerTransactionAccount o1, LedgerTransactionAccount o2) {
-                    int res = o1.getAccountName()
-                                .compareTo(o2.getAccountName());
-                    if (res != 0)
-                        return res;
-                    res = o1.getCurrency()
-                            .compareTo(o2.getCurrency());
-                    if (res != 0)
-                        return res;
-                    return Float.compare(o1.getAmount(), o2.getAmount());
-                }
-            };
-    private String profile;
-    private Integer id;
-    private Date date;
+    public final Comparator<LedgerTransactionAccount> comparator = (o1, o2) -> {
+        int res = o1.getAccountName()
+                    .compareTo(o2.getAccountName());
+        if (res != 0)
+            return res;
+        res = o1.getCurrency()
+                .compareTo(o2.getCurrency());
+        if (res != 0)
+            return res;
+        res = o1.getComment()
+                .compareTo(o2.getComment());
+        if (res != 0)
+            return res;
+        return Float.compare(o1.getAmount(), o2.getAmount());
+    };
+    private final long profile;
+    private final long ledgerId;
+    private final List<LedgerTransactionAccount> accounts;
+    private long dbId;
+    private SimpleDate date;
     private String description;
     private String description;
-    private ArrayList<LedgerTransactionAccount> accounts;
+    private String comment;
     private String dataHash;
     private boolean dataLoaded;
     private String dataHash;
     private boolean dataLoaded;
-    public LedgerTransaction(Integer id, String dateString, String description)
+    public LedgerTransaction(long ledgerId, String dateString, String description)
             throws ParseException {
             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 LedgerTransaction(Integer id, Date date, String description,
-                             MobileLedgerProfile profile) {
-        this.profile = profile.getUuid();
-        this.id = id;
+    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(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;
     }
         this.date = date;
         this.description = description;
         this.accounts = new ArrayList<>();
         this.dataHash = null;
         dataLoaded = false;
     }
-    public LedgerTransaction(Integer id, Date date, String description) {
-        this(id, date, description, Data.profile.getValue());
+    public LedgerTransaction(long ledgerId, SimpleDate date, String description) {
+        this(ledgerId, date, description, Data.getProfile());
     }
     }
-    public LedgerTransaction(Date date, String description) {
-        this(null, date, description);
+    public LedgerTransaction(SimpleDate date, String description) {
+        this(0, date, description);
     }
     }
-    public LedgerTransaction(int id) {
-        this(id, (Date) 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<>();
         this.dataHash = null;
         this.dataLoaded = false;
     }
         this.date = null;
         this.description = null;
         this.accounts = new ArrayList<>();
         this.dataHash = null;
         this.dataLoaded = false;
     }
-    public ArrayList<LedgerTransactionAccount> getAccounts() {
+    public List<LedgerTransactionAccount> getAccounts() {
         return accounts;
     }
     public void addAccount(LedgerTransactionAccount item) {
         accounts.add(item);
         dataHash = null;
     }
         return accounts;
     }
     public void addAccount(LedgerTransactionAccount item) {
         accounts.add(item);
         dataHash = null;
     }
-    public Date getDate() {
+    @Nullable
+    public SimpleDate getDateIfAny() {
         return date;
     }
         return date;
     }
-    public void setDate(Date date) {
+    @NonNull
+    public SimpleDate getDate() {
+        if (date == null)
+            throw new IllegalStateException("Transaction has no date");
+        return date;
+    }
+    public void setDate(SimpleDate date) {
         this.date = date;
         dataHash = null;
     }
         this.date = date;
         dataHash = null;
     }
@@ -107,8 +158,14 @@ public class LedgerTransaction {
         this.description = description;
         dataHash = null;
     }
         this.description = description;
         dataHash = null;
     }
-    public int getId() {
-        return id;
+    public String getComment() {
+        return comment;
+    }
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
+    public long getLedgerId() {
+        return ledgerId;
     }
     protected void fillDataHash() {
         if (dataHash != null)
     }
     protected void fillDataHash() {
         if (dataHash != null)
@@ -116,11 +173,14 @@ public class LedgerTransaction {
         try {
             Digest sha = new Digest(DIGEST_TYPE);
             StringBuilder data = new StringBuilder();
         try {
             Digest sha = new Digest(DIGEST_TYPE);
             StringBuilder data = new StringBuilder();
+            data.append("ver1");
             data.append(profile);
             data.append(profile);
-            data.append(getId());
+            data.append(getLedgerId());
             data.append('\0');
             data.append(getDescription());
             data.append('\0');
             data.append('\0');
             data.append(getDescription());
             data.append('\0');
+            data.append(getComment());
+            data.append('\0');
             data.append(Globals.formatLedgerDate(getDate()));
             data.append('\0');
             for (LedgerTransactionAccount item : accounts) {
             data.append(Globals.formatLedgerDate(getDate()));
             data.append('\0');
             for (LedgerTransactionAccount item : accounts) {
@@ -129,9 +189,11 @@ public class LedgerTransaction {
                 data.append(item.getCurrency());
                 data.append('\0');
                 data.append(item.getAmount());
                 data.append(item.getCurrency());
                 data.append('\0');
                 data.append(item.getAmount());
+                data.append('\0');
+                data.append(item.getComment());
             }
             sha.update(data.toString()
             }
             sha.update(data.toString()
-                           .getBytes(Charset.forName("UTF-8")));
+                           .getBytes(StandardCharsets.UTF_8));
             dataHash = sha.digestToHexString();
         }
         catch (NoSuchAlgorithmException e) {
             dataHash = sha.digestToHexString();
         }
         catch (NoSuchAlgorithmException e) {
@@ -139,61 +201,37 @@ public class LedgerTransaction {
                     String.format("Unable to get instance of %s digest", DIGEST_TYPE), e);
         }
     }
                     String.format("Unable to get instance of %s digest", DIGEST_TYPE), e);
         }
     }
-    public boolean existsInDb(SQLiteDatabase db) {
+    public String getDataHash() {
         fillDataHash();
         fillDataHash();
-        try (Cursor c = db.rawQuery("SELECT 1 from transactions where data_hash = ?",
-                new String[]{dataHash}))
-        {
-            boolean result = c.moveToFirst();
-//            debug("db", String.format("Transaction %d (%s) %s", id, dataHash,
-//                    result ? "already present" : "not present"));
-            return result;
-        }
+        return dataHash;
     }
     }
-    public void loadData(SQLiteDatabase db) {
-        if (dataLoaded)
-            return;
-
-        try (Cursor cTr = db.rawQuery(
-                "SELECT date, description from transactions WHERE profile=? AND id=?",
-                new String[]{profile, String.valueOf(id)}))
-        {
-            if (cTr.moveToFirst()) {
-                String dateString = cTr.getString(0);
-                try {
-                    date = Globals.parseLedgerDate(dateString);
-                }
-                catch (ParseException e) {
-                    e.printStackTrace();
-                    throw new RuntimeException(
-                            String.format("Error parsing date '%s' from " + "transacion %d",
-                                    dateString, id));
-                }
-                description = cTr.getString(1);
+    public void finishLoading() {
+        dataLoaded = true;
+    }
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null)
+            return false;
+        if (!obj.getClass()
+                .equals(this.getClass()))
+            return false;
 
 
-                try (Cursor cAcc = db.rawQuery("SELECT account_name, amount, currency 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)));
-                    }
+        return ((LedgerTransaction) obj).getDataHash()
+                                        .equals(getDataHash());
+    }
 
 
-                    finishLoading();
-                }
-            }
+    public boolean hasAccountNamedLike(String name) {
+        name = name.toUpperCase();
+        for (LedgerTransactionAccount acc : accounts) {
+            if (acc.getAccountName()
+                   .toUpperCase()
+                   .contains(name))
+                return true;
         }
 
         }
 
+        return false;
     }
     }
-    public String getDataHash() {
-        return dataHash;
-    }
-    public void finishLoading() {
+    public void markDataAsLoaded() {
         dataLoaded = true;
     }
 }
         dataLoaded = true;
     }
 }
index 89d6a83a5d3c935ce98b0dcaa65c8d82fa0b6410..9706dbb5633e0d8428c98444156a942ee4cd9bd5 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 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;
 
 public class LedgerTransactionAccount {
     private String accountName;
     private String shortAccountName;
     private float amount;
     private boolean amountSet = false;
 
 public class LedgerTransactionAccount {
     private String accountName;
     private String shortAccountName;
     private float amount;
     private boolean amountSet = false;
+    @Nullable
     private String currency;
     private String currency;
-
-    public LedgerTransactionAccount(String accountName, float amount) {
-        this(accountName, amount, null);
-    }
-    public LedgerTransactionAccount(String accountName, float amount, 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);
         this.amount = amount;
         this.amountSet = true;
         this.setAccountName(accountName);
         this.amount = amount;
         this.amountSet = true;
-        this.currency = currency;
+        this.amountValid = true;
+        this.currency = Misc.emptyIsNull(currency);
+        this.comment = Misc.emptyIsNull(comment);
     }
     }
-
     public LedgerTransactionAccount(String accountName) {
         this.accountName = accountName;
     }
     public LedgerTransactionAccount(String accountName) {
         this.accountName = accountName;
     }
+    public LedgerTransactionAccount(String accountName, String currency) {
+        this.accountName = accountName;
+        this.currency = Misc.emptyIsNull(currency);
+    }
     public LedgerTransactionAccount(LedgerTransactionAccount origin) {
         // copy constructor
         setAccountName(origin.getAccountName());
     public LedgerTransactionAccount(LedgerTransactionAccount origin) {
         // copy constructor
         setAccountName(origin.getAccountName());
+        setComment(origin.getComment());
         if (origin.isAmountSet())
             setAmount(origin.getAmount());
         if (origin.isAmountSet())
             setAmount(origin.getAmount());
+        amountValid = origin.amountValid;
         currency = origin.getCurrency();
     }
         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;
+    }
+    public void setComment(String comment) {
+        this.comment = comment;
+    }
     public String getAccountName() {
         return accountName;
     }
     public String getAccountName() {
         return accountName;
     }
@@ -63,22 +89,29 @@ public class LedgerTransactionAccount {
 
         return amount;
     }
 
         return amount;
     }
-
     public void setAmount(float account_amount) {
         this.amount = account_amount;
         this.amountSet = true;
     public void setAmount(float account_amount) {
         this.amount = account_amount;
         this.amountSet = true;
+        this.amountValid = true;
     }
     }
-
     public void resetAmount() {
         this.amountSet = false;
     public void resetAmount() {
         this.amountSet = false;
+        this.amountValid = true;
+    }
+    public void invalidateAmount() {
+        this.amountValid = false;
     }
     }
-
     public boolean isAmountSet() {
         return amountSet;
     }
     public boolean isAmountSet() {
         return amountSet;
     }
+    public boolean isAmountValid() { return amountValid; }
+    @Nullable
     public String getCurrency() {
         return currency;
     }
     public String getCurrency() {
         return currency;
     }
+    public void setCurrency(String currency) {
+        this.currency = Misc.emptyIsNull(currency);
+    }
     @NonNull
     public String toString() {
         if (!amountSet)
     @NonNull
     public String toString() {
         if (!amountSet)
@@ -89,8 +122,19 @@ public class LedgerTransactionAccount {
             sb.append(currency);
             sb.append(' ');
         }
             sb.append(currency);
             sb.append(' ');
         }
-        sb.append(String.format("%,1.2f", amount));
+        sb.append(String.format(Locale.US, "%,1.2f", amount));
 
         return sb.toString();
     }
 
         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
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java b/app/src/main/java/net/ktnx/mobileledger/model/MatchedTemplate.java
new file mode 100644 (file)
index 0000000..607d6c7
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java b/app/src/main/java/net/ktnx/mobileledger/model/MobileLedgerProfile.java
deleted file mode 100644 (file)
index 1f2eac7..0000000
+++ /dev/null
@@ -1,532 +0,0 @@
-/*
- * 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.model;
-
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-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.Globals;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.UUID;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public final class MobileLedgerProfile {
-    private String uuid;
-    private String name;
-    private boolean permitPosting;
-    private String preferredAccountsFilter;
-    private String url;
-    private boolean authEnabled;
-    private String authUserName;
-    private String authPassword;
-    private int themeId;
-    private int orderNo = -1;
-    private FutureDates futureDates = FutureDates.None;
-    private SendTransactionTask.API apiVersion = SendTransactionTask.API.auto;
-    public MobileLedgerProfile() {
-        this.uuid = String.valueOf(UUID.randomUUID());
-    }
-    public MobileLedgerProfile(String uuid) {
-        this.uuid = uuid;
-    }
-    public MobileLedgerProfile(MobileLedgerProfile origin) {
-        uuid = origin.uuid;
-        name = origin.name;
-        permitPosting = origin.permitPosting;
-        preferredAccountsFilter = origin.preferredAccountsFilter;
-        url = origin.url;
-        authEnabled = origin.authEnabled;
-        authUserName = origin.authUserName;
-        authPassword = origin.authPassword;
-        themeId = origin.themeId;
-        orderNo = origin.orderNo;
-        futureDates = origin.futureDates;
-        apiVersion = origin.apiVersion;
-    }
-    // loads all profiles into Data.profiles
-    // returns the profile with the given UUID
-    public static MobileLedgerProfile loadAllFromDB(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 " +
-                                         "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));
-                list.add(item);
-                if (item.getUuid()
-                        .equals(currentProfileUUID))
-                    result = item;
-            }
-        }
-        Data.profiles.setValue(list);
-        return result;
-    }
-    public static void storeProfilesOrder() {
-        SQLiteDatabase db = App.getDatabase();
-        db.beginTransaction();
-        try {
-            int orderNo = 0;
-            for (MobileLedgerProfile p : 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 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.beginTransaction();
-        try {
-//            debug("profiles", String.format("Storing profile in DB: uuid=%s, name=%s, " +
-//                                            "url=%s, permit_posting=%s, authEnabled=%s, " +
-//                                            "themeId=%d", uuid, name, url,
-//                    permitPosting ? "TRUE" : "FALSE", authEnabled ? "TRUE" : "FALSE", themeId));
-            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) " + "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
-                    new Object[]{uuid, name, permitPosting, url, authEnabled,
-                                 authEnabled ? authUserName : null,
-                                 authEnabled ? authPassword : null, themeId, orderNo,
-                                 preferredAccountsFilter, futureDates.toInt(), apiVersion.toInt()
-                    });
-            db.setTransactionSuccessful();
-        }
-        finally {
-            db.endTransaction();
-        }
-    }
-    public void storeAccount(SQLiteDatabase db, LedgerAccount acc) {
-        // replace into is a bad idea because it would reset hidden to its default value
-        // we like the default, but for new accounts only
-        db.execSQL("update accounts set level = ?, keep = 1, hidden=?, expanded=? " +
-                   "where profile=? and name = ?",
-                new Object[]{acc.getLevel(), acc.isHiddenByStar(), acc.isExpanded(), uuid,
-                             acc.getName()
-                });
-        db.execSQL("insert into accounts(profile, name, name_upper, parent_name, level, hidden, " +
-                   "expanded, keep) " + "select ?,?,?,?,?,?,?,1 where (select changes() = 0)",
-                new Object[]{uuid, acc.getName(), acc.getName().toUpperCase(), acc.getParentName(),
-                             acc.getLevel(), acc.isHiddenByStar(), acc.isExpanded()
-                });
-//        debug("accounts", String.format("Stored account '%s' in DB [%s]", acc.getName(), uuid));
-    }
-    public void storeAccountValue(SQLiteDatabase db, String name, String currency, Float amount) {
-        db.execSQL("replace into account_values(profile, account, " +
-                   "currency, value, keep) values(?, ?, ?, ?, 1);",
-                new Object[]{uuid, name, currency, amount});
-    }
-    public void storeTransaction(SQLiteDatabase db, LedgerTransaction tr) {
-        tr.fillDataHash();
-        db.execSQL("DELETE from transactions WHERE profile=? and id=?",
-                new Object[]{uuid, tr.getId()});
-        db.execSQL("DELETE from transaction_accounts WHERE profile = ? and transaction_id=?",
-                new Object[]{uuid, tr.getId()});
-
-        db.execSQL("INSERT INTO transactions(profile, id, date, description, data_hash, keep) " +
-                   "values(?,?,?,?,?,1)",
-                new Object[]{uuid, tr.getId(), Globals.formatLedgerDate(tr.getDate()),
-                             tr.getDescription(), tr.getDataHash()
-                });
-
-        for (LedgerTransactionAccount item : tr.getAccounts()) {
-            db.execSQL("INSERT INTO transaction_accounts(profile, transaction_id, " +
-                       "account_name, amount, currency) values(?, ?, ?, ?, ?)",
-                    new Object[]{uuid, tr.getId(), item.getAccountName(), item.getAmount(),
-                                 item.getCurrency()
-                    });
-        }
-//        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.beginTransaction();
-        try {
-            Object[] uuid_param = new Object[]{uuid};
-            db.execSQL("delete from profiles where uuid=?", uuid_param);
-            db.execSQL("delete from accounts where profile=?", uuid_param);
-            db.execSQL("delete from account_values where profile=?", uuid_param);
-            db.execSQL("delete from transactions where profile=?", uuid_param);
-            db.execSQL("delete from transaction_accounts where profile=?", uuid_param);
-            db.execSQL("delete from options where profile=?", uuid_param);
-            db.setTransactionSuccessful();
-        }
-        finally {
-            db.endTransaction();
-        }
-    }
-    @NonNull
-    public LedgerAccount loadAccount(String name) {
-        SQLiteDatabase db = App.getDatabase();
-        return loadAccount(db, name);
-    }
-    @Nullable
-    public LedgerAccount tryLoadAccount(String acct_name) {
-        SQLiteDatabase db = App.getDatabase();
-        return tryLoadAccount(db, acct_name);
-    }
-    @NonNull
-    public LedgerAccount loadAccount(SQLiteDatabase db, String accName) {
-        LedgerAccount acc = tryLoadAccount(db, accName);
-
-        if (acc == null)
-            throw new RuntimeException("Unable to load account with name " + accName);
-
-        return acc;
-    }
-    @Nullable
-    public LedgerAccount tryLoadAccount(SQLiteDatabase db, String accName) {
-        try (Cursor cursor = db.rawQuery(
-                "SELECT a.hidden, a.expanded, (select 1 from accounts a2 " +
-                "where a2.profile = a.profile and a2.name like a.name||':%' limit 1) " +
-                "FROM accounts a WHERE a.profile = ? and a.name=?", new String[]{uuid, accName}))
-        {
-            if (cursor.moveToFirst()) {
-                LedgerAccount acc = new LedgerAccount(accName);
-                acc.setHiddenByStar(cursor.getInt(0) == 1);
-                acc.setExpanded(cursor.getInt(1) == 1);
-                acc.setHasSubAccounts(cursor.getInt(2) == 1);
-
-                try (Cursor c2 = db.rawQuery(
-                        "SELECT value, currency FROM account_values WHERE profile = ? " +
-                        "AND account = ?", new String[]{uuid, accName}))
-                {
-                    while (c2.moveToNext()) {
-                        acc.addAmount(c2.getFloat(0), c2.getString(1));
-                    }
-                }
-
-                return acc;
-            }
-            return null;
-        }
-    }
-    public LedgerTransaction loadTransaction(int transactionId) {
-        LedgerTransaction tr = new LedgerTransaction(transactionId, this.uuid);
-        tr.loadData(App.getDatabase());
-
-        return tr;
-    }
-    public int getThemeId() {
-//        debug("profile", String.format("Profile.getThemeId() returning %d", themeId));
-        return this.themeId;
-    }
-    public void setThemeId(Object o) {
-        setThemeId(Integer.valueOf(String.valueOf(o))
-                          .intValue());
-    }
-    public void setThemeId(int themeId) {
-//        debug("profile", String.format("Profile.setThemeId(%d) called", themeId));
-        this.themeId = themeId;
-    }
-    public void markTransactionsAsNotPresent(SQLiteDatabase db) {
-        db.execSQL("UPDATE transactions set keep=0 where profile=?", new String[]{uuid});
-
-    }
-    public void markAccountsAsNotPresent(SQLiteDatabase db) {
-        db.execSQL("update account_values set keep=0 where profile=?;", new String[]{uuid});
-        db.execSQL("update accounts set keep=0 where profile=?;", new String[]{uuid});
-
-    }
-    public void deleteNotPresentAccounts(SQLiteDatabase db) {
-        db.execSQL("delete from account_values where keep=0 and profile=?", new String[]{uuid});
-        db.execSQL("delete from accounts where keep=0 and profile=?", new String[]{uuid});
-    }
-    public void markTransactionAsPresent(SQLiteDatabase db, LedgerTransaction transaction) {
-        db.execSQL("UPDATE transactions SET keep = 1 WHERE profile = ? and id=?",
-                new Object[]{uuid, transaction.getId()
-                });
-    }
-    public void markTransactionsBeforeTransactionAsPresent(SQLiteDatabase db,
-                                                           LedgerTransaction transaction) {
-        db.execSQL("UPDATE transactions SET keep=1 WHERE profile = ? and id < ?",
-                new Object[]{uuid, transaction.getId()
-                });
-
-    }
-    public void deleteNotPresentTransactions(SQLiteDatabase db) {
-        db.execSQL("DELETE FROM transactions WHERE profile=? AND keep = 0", new String[]{uuid});
-    }
-    public void setLastUpdateStamp() {
-        debug("db", "Updating transaction value stamp");
-        Date now = new Date();
-        setLongOption(MLDB.OPT_LAST_SCRAPE, now.getTime());
-        Data.lastUpdateDate.postValue(now);
-    }
-    public List<LedgerAccount> loadChildAccountsOf(LedgerAccount acc) {
-        List<LedgerAccount> result = new ArrayList<>();
-        SQLiteDatabase db = App.getDatabase();
-        try (Cursor c = db.rawQuery(
-                "SELECT a.name FROM accounts a WHERE a.profile = ? and a.name like ?||':%'",
-                new String[]{uuid, acc.getName()}))
-        {
-            while (c.moveToNext()) {
-                LedgerAccount a = loadAccount(db, c.getString(0));
-                result.add(a);
-            }
-        }
-
-        return result;
-    }
-    public List<LedgerAccount> loadVisibleChildAccountsOf(LedgerAccount acc) {
-        List<LedgerAccount> result = new ArrayList<>();
-        ArrayList<LedgerAccount> visibleList = new ArrayList<>();
-        visibleList.add(acc);
-
-        SQLiteDatabase db = App.getDatabase();
-        try (Cursor c = db.rawQuery(
-                "SELECT a.name FROM accounts a WHERE a.profile = ? and a.name like ?||':%'",
-                new String[]{uuid, acc.getName()}))
-        {
-            while (c.moveToNext()) {
-                LedgerAccount a = loadAccount(db, c.getString(0));
-                if (a.isVisible(visibleList)) {
-                    result.add(a);
-                    visibleList.add(a);
-                }
-            }
-        }
-
-        return result;
-    }
-    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();
-            Logger.debug("wipe", String.format(Locale.ENGLISH, "Profile %s wiped out", pUuid[0]));
-        }
-        finally {
-            db.endTransaction();
-        }
-    }
-    public enum FutureDates {
-        None(0), OneMonth(30), TwoMonths(60), ThreeMonths(90), SixMonths(180), OneYear(365),
-        All(-1);
-        private static 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 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);
-            }
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailSource.java
new file mode 100644 (file)
index 0000000..611bcfc
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java b/app/src/main/java/net/ktnx/mobileledger/model/TemplateDetailsItem.java
new file mode 100644 (file)
index 0000000..9a8f04c
--- /dev/null
@@ -0,0 +1,758 @@
+/*
+ * 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;
+        }
+    }
+}
index e24647f22361205d6cd63b477873f2ad45deb8ca..7c3520919232c10c7fc44c42fe19957c649231e4 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.model;
 
-import java.util.Date;
-
 import androidx.annotation.NonNull;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import org.jetbrains.annotations.NotNull;
 
 public class TransactionListItem {
 
 public class TransactionListItem {
-    private Type type;
-    private Date date;
+    private final Type type;
+    private SimpleDate date;
     private boolean monthShown;
     private LedgerTransaction transaction;
     private boolean monthShown;
     private LedgerTransaction transaction;
-    private boolean odd;
-    public TransactionListItem(Date date, boolean monthShown) {
+    private String boldAccountName;
+    private String runningTotal;
+    public TransactionListItem(@NotNull SimpleDate date, boolean monthShown) {
         this.type = Type.DELIMITER;
         this.date = date;
         this.monthShown = monthShown;
     }
         this.type = Type.DELIMITER;
         this.date = date;
         this.monthShown = monthShown;
     }
-    public TransactionListItem(LedgerTransaction transaction, boolean isOdd) {
+    public TransactionListItem(@NotNull LedgerTransaction transaction,
+                               @Nullable String boldAccountName, @Nullable String runningTotal) {
         this.type = Type.TRANSACTION;
         this.transaction = transaction;
         this.type = Type.TRANSACTION;
         this.transaction = transaction;
-        this.odd = isOdd;
+        this.boldAccountName = boldAccountName;
+        this.runningTotal = runningTotal;
+    }
+    public TransactionListItem() {
+        this.type = Type.HEADER;
+    }
+    public String getRunningTotal() {
+        return runningTotal;
     }
     @NonNull
     public Type getType() {
         return type;
     }
     }
     @NonNull
     public Type getType() {
         return type;
     }
-    public Date getDate() {
-        return date;
+    @NonNull
+    public SimpleDate getDate() {
+        if (date != null)
+            return date;
+        if (type != Type.TRANSACTION)
+            throw new IllegalStateException("Only transaction items have a date");
+        return transaction.getDate();
     }
     public boolean isMonthShown() {
         return monthShown;
     }
     }
     public boolean isMonthShown() {
         return monthShown;
     }
+    @NotNull
     public LedgerTransaction getTransaction() {
     public LedgerTransaction getTransaction() {
+        if (type != Type.TRANSACTION)
+            throw new IllegalStateException(
+                    String.format("Item type is not %s, but %s", Type.TRANSACTION, type));
         return transaction;
     }
         return transaction;
     }
-    public boolean isOdd() {
-        return odd;
+    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);
+        }
     }
     }
-    public enum Type {TRANSACTION, DELIMITER}
 }
 }
index 8b73de1d331c68c4ff7207c2e2913ae446c82332..78e62d4cf2d7813842af6a1a7980b0ac0d414097 100644 (file)
@@ -57,7 +57,7 @@ public final class AutoCompleteTextViewWithClear extends AppCompatAutoCompleteTe
         setCompoundDrawablesRelative(null, null, null, null);
     }
     private void showClearDrawable() {
         setCompoundDrawablesRelative(null, null, null, null);
     }
     private void showClearDrawable() {
-        setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_clear_black_24dp, 0);
+        setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_clear_accent_24dp, 0);
     }
     @Override
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
     }
     @Override
     protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
index b87268ee6836fd98998b0ca0be97426f20753ccb..0840a4b5dc47bebc620497038bc96882615bda51 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,17 +19,17 @@ package net.ktnx.mobileledger.ui;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
 import android.content.Intent;
 import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.DialogFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.utils.Globals;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.utils.Globals;
 
@@ -48,50 +48,33 @@ public class CrashReportDialogFragment extends DialogFragment {
         View view = inflater.inflate(R.layout.crash_dialog, null);
         ((TextView) view.findViewById(R.id.textCrashReport)).setText(mCrashReportText);
         repScroll = view.findViewById(R.id.scrollText);
         View view = inflater.inflate(R.layout.crash_dialog, null);
         ((TextView) view.findViewById(R.id.textCrashReport)).setText(mCrashReportText);
         repScroll = view.findViewById(R.id.scrollText);
-        builder.setTitle(R.string.crash_dialog_title).setView(view)
-                .setPositiveButton(R.string.btn_send_crash_report,
-                        new DialogInterface.OnClickListener() {
-                            @Override
-                            public void onClick(DialogInterface dialog, int which) {
-                                // still nothing
-                                Intent email = new Intent(Intent.ACTION_SEND);
-                                email.putExtra(Intent.EXTRA_EMAIL,
-                                        new String[]{Globals.developerEmail});
-                                email.putExtra(Intent.EXTRA_SUBJECT, "MoLe crash report");
-                                email.putExtra(Intent.EXTRA_TEXT, mCrashReportText);
-                                email.setType("message/rfc822");
-                                startActivity(Intent.createChooser(email,
-                                        getResources().getString(R.string.send_crash_via)));
-                            }
-                        })
-                .setNegativeButton(R.string.btn_not_now, new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                        CrashReportDialogFragment.this.getDialog().cancel();
-                    }
-                })
-                .setNeutralButton(R.string.btn_show_report, new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(DialogInterface dialog, int which) {
-                    }
-                });
+        builder.setTitle(R.string.crash_dialog_title)
+               .setView(view)
+               .setPositiveButton(R.string.btn_send_crash_report, (dialog, which) -> {
+                   // still nothing
+                   Intent email = new Intent(Intent.ACTION_SEND);
+                   email.putExtra(Intent.EXTRA_EMAIL, new String[]{Globals.developerEmail});
+                   email.putExtra(Intent.EXTRA_SUBJECT, "MoLe crash report");
+                   email.putExtra(Intent.EXTRA_TEXT, mCrashReportText);
+                   email.setType("message/rfc822");
+                   startActivity(Intent.createChooser(email,
+                           getResources().getString(R.string.send_crash_via)));
+               })
+               .setNegativeButton(R.string.btn_not_now,
+                       (dialog, which) -> CrashReportDialogFragment.this.getDialog()
+                                                                        .cancel())
+               .setNeutralButton(R.string.btn_show_report, (dialog, which) -> {
+               });
 
         AlertDialog dialog = builder.create();
 
         AlertDialog dialog = builder.create();
-        dialog.setOnShowListener(new DialogInterface.OnShowListener() {
-            @Override
-            public void onShow(DialogInterface dialogIinterface) {
-                dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
-                        .setOnClickListener(new View.OnClickListener() {
-                            @Override
-                            public void onClick(View v) {
-                                if (repScroll != null) {
-                                    repScroll.setVisibility(View.VISIBLE);
-                                    v.setVisibility(View.GONE);
-                                }
-                            }
-                        });
-            }
-        });
+        dialog.setOnShowListener(dialogInterface -> dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
+                                                          .setOnClickListener(v -> {
+                                                              if (repScroll != null) {
+                                                                  repScroll.setVisibility(
+                                                                          View.VISIBLE);
+                                                                  v.setVisibility(View.GONE);
+                                                              }
+                                                          }));
         return dialog;
     }
     @Override
         return dialog;
     }
     @Override
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorFragment.java
new file mode 100644 (file)
index 0000000..759cd90
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ * 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.View;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+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 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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A fragment representing a list of Items.
+ * <p/>
+ * Activities containing this fragment MUST implement the {@link OnCurrencySelectedListener}
+ * interface.
+ */
+public class CurrencySelectorFragment extends AppCompatDialogFragment
+        implements OnCurrencySelectedListener, OnCurrencyLongClickListener {
+
+    public static final int DEFAULT_COLUMN_COUNT = 2;
+    public static final String ARG_COLUMN_COUNT = "column-count";
+    public static final String ARG_SHOW_PARAMS = "show-params";
+    public static final boolean DEFAULT_SHOW_PARAMS = true;
+    private int mColumnCount = DEFAULT_COLUMN_COUNT;
+    private CurrencySelectorModel model;
+    private boolean deferredShowPositionAndPadding;
+    private OnCurrencySelectedListener onCurrencySelectedListener;
+
+    /**
+     * Mandatory empty constructor for the fragment manager to instantiate the
+     * fragment (e.g. upon screen orientation changes).
+     */
+    public CurrencySelectorFragment() {
+    }
+    @SuppressWarnings("unused")
+    public static CurrencySelectorFragment newInstance() {
+        return newInstance(DEFAULT_COLUMN_COUNT, DEFAULT_SHOW_PARAMS);
+    }
+    public static CurrencySelectorFragment newInstance(int columnCount, boolean showParams) {
+        CurrencySelectorFragment fragment = new CurrencySelectorFragment();
+        Bundle args = new Bundle();
+        args.putInt(ARG_COLUMN_COUNT, columnCount);
+        args.putBoolean(ARG_SHOW_PARAMS, showParams);
+        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);
+        }
+    }
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        Context context = requireContext();
+        Dialog csd = new Dialog(context);
+        csd.setContentView(R.layout.fragment_currency_selector_list);
+        csd.setTitle(R.string.choose_currency_label);
+
+        RecyclerView recyclerView = csd.findViewById(R.id.list);
+
+        if (mColumnCount <= 1) {
+            recyclerView.setLayoutManager(new LinearLayoutManager(context));
+        }
+        else {
+            recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount));
+        }
+        model = new ViewModelProvider(this).get(CurrencySelectorModel.class);
+        if (onCurrencySelectedListener != null)
+            model.setOnCurrencySelectedListener(onCurrencySelectedListener);
+        Profile profile = Data.getProfile();
+
+        CurrencySelectorRecyclerViewAdapter adapter = new CurrencySelectorRecyclerViewAdapter();
+        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);
+        adapter.setCurrencyLongClickListener(this);
+
+        final TextView tvNewCurrName = csd.findViewById(R.id.new_currency_name);
+        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);
+        tvNoCurrBtn.setVisibility(View.VISIBLE);
+        tvAddCurrBtn.setVisibility(View.VISIBLE);
+
+        tvAddCurrBtn.setOnClickListener(v -> {
+            tvNewCurrName.setVisibility(View.VISIBLE);
+            tvAddCurrOkBtn.setVisibility(View.VISIBLE);
+
+            tvNoCurrBtn.setVisibility(View.GONE);
+            tvAddCurrBtn.setVisibility(View.GONE);
+
+            tvNewCurrName.setText(null);
+            tvNewCurrName.requestFocus();
+            net.ktnx.mobileledger.utils.Misc.showSoftKeyboard(this);
+        });
+
+        tvAddCurrOkBtn.setOnClickListener(v -> {
+            String currName = String.valueOf(tvNewCurrName.getText());
+            if (!currName.isEmpty()) {
+                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);
+            tvAddCurrOkBtn.setVisibility(View.GONE);
+
+            tvNoCurrBtn.setVisibility(View.VISIBLE);
+            tvAddCurrBtn.setVisibility(View.VISIBLE);
+        });
+
+        tvNoCurrBtn.setOnClickListener(v -> {
+            adapter.notifyCurrencySelected(null);
+            dismiss();
+        });
+
+        RadioButton rbPositionLeft = csd.findViewById(R.id.currency_position_left);
+        RadioButton rbPositionRight = csd.findViewById(R.id.currency_position_right);
+
+        if (Data.currencySymbolPosition.getValue() == Currency.Position.before)
+            rbPositionLeft.toggle();
+        else
+            rbPositionRight.toggle();
+
+        rgPosition.setOnCheckedChangeListener((group, checkedId) -> {
+            if (checkedId == R.id.currency_position_left)
+                Data.currencySymbolPosition.setValue(Currency.Position.before);
+            else
+                Data.currencySymbolPosition.setValue(Currency.Position.after);
+        });
+
+        gap.setChecked(Data.currencyGap.getValue());
+
+        gap.setOnCheckedChangeListener((v, checked) -> Data.currencyGap.setValue(checked));
+
+        model.observePositionAndPaddingVisible(this, visible -> csd.findViewById(R.id.params_panel)
+                                                                   .setVisibility(
+                                                                           visible ? View.VISIBLE
+                                                                                   : View.GONE));
+
+        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();
+
+        return csd;
+    }
+    public void setOnCurrencySelectedListener(OnCurrencySelectedListener listener) {
+        onCurrencySelectedListener = listener;
+
+        if (model != null)
+            model.setOnCurrencySelectedListener(listener);
+    }
+    public void resetOnCurrencySelectedListener() {
+        model.resetOnCurrencySelectedListener();
+    }
+    @Override
+    public void onCurrencySelected(String item) {
+        model.triggerOnCurrencySelectedListener(item);
+
+        dismiss();
+    }
+
+    @Override
+    public void onCurrencyLongClick(String item) {
+        CurrencyDAO dao = DB.get()
+                            .getCurrencyDAO();
+        dao.getByName(item)
+           .observe(this, dao::deleteSync);
+    }
+    public void showPositionAndPadding() {
+        deferredShowPositionAndPadding = true;
+    }
+    public void hidePositionAndPadding() {
+        deferredShowPositionAndPadding = false;
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorModel.java
new file mode 100644 (file)
index 0000000..0fca9d6
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+public class CurrencySelectorModel extends ViewModel {
+    private final MutableLiveData<Boolean> positionAndPaddingVisible = new MutableLiveData<>(true);
+    private OnCurrencySelectedListener selectionListener;
+    public CurrencySelectorModel() { }
+    public void showPositionAndPadding() {
+        positionAndPaddingVisible.postValue(true);
+    }
+    public void hidePositionAndPadding() {
+        positionAndPaddingVisible.postValue(false);
+    }
+    public void observePositionAndPaddingVisible(LifecycleOwner activity,
+                                                 Observer<Boolean> observer) {
+        positionAndPaddingVisible.observe(activity, observer);
+    }
+    void setOnCurrencySelectedListener(OnCurrencySelectedListener listener) {
+        selectionListener = listener;
+    }
+    void resetOnCurrencySelectedListener() {
+        selectionListener = null;
+    }
+    void triggerOnCurrencySelectedListener(String c) {
+        if (selectionListener != null)
+            selectionListener.onCurrencySelected(c);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/CurrencySelectorRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..7d4c60b
--- /dev/null
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import android.view.LayoutInflater;
+import android.view.View;
+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;
+
+import net.ktnx.mobileledger.R;
+import net.ktnx.mobileledger.model.Currency;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * {@link RecyclerView.Adapter} that can display a {@link Currency} and makes a call to the
+ * specified {@link OnCurrencySelectedListener}.
+ */
+public class CurrencySelectorRecyclerViewAdapter
+        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(DIFF_CALLBACK);
+    }
+    @NotNull
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        View view = LayoutInflater.from(parent.getContext())
+                                  .inflate(R.layout.fragment_currency_selector, parent, false);
+        return new ViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(final ViewHolder holder, int position) {
+        holder.bindTo(getItem(position));
+    }
+    public void setCurrencySelectedListener(OnCurrencySelectedListener listener) {
+        this.currencySelectedListener = listener;
+    }
+    public void resetCurrencySelectedListener() {
+        currencySelectedListener = null;
+    }
+    public void notifyCurrencySelected(String currency) {
+        if (null != currencySelectedListener)
+            currencySelectedListener.onCurrencySelected(currency);
+    }
+    public void setCurrencyLongClickListener(OnCurrencyLongClickListener listener) {
+        this.currencyLongClickListener = listener;
+    }
+    public void resetCurrencyLockClickListener() { currencyLongClickListener = null; }
+    private void notifyCurrencyLongClicked(String mItem) {
+        if (null != currencyLongClickListener)
+            currencyLongClickListener.onCurrencyLongClick(mItem);
+    }
+
+    public class ViewHolder extends RecyclerView.ViewHolder {
+        private final TextView mNameView;
+        private String mItem;
+
+        ViewHolder(View view) {
+            super(view);
+            mNameView = view.findViewById(R.id.content);
+
+            view.setOnClickListener(v -> notifyCurrencySelected(mItem));
+            view.setOnLongClickListener(v -> {
+                notifyCurrencyLongClicked(mItem);
+                return false;
+            });
+        }
+
+        @NotNull
+        @Override
+        public String toString() {
+            return super.toString() + " '" + mNameView.getText() + "'";
+        }
+        void bindTo(String item) {
+            mItem = item;
+            mNameView.setText(item);
+        }
+    }
+}
index b300ea6ce4e75b364b8c7bdf526900cb5c7ac4d9..54a2093247e47ccf73982b1c64d5ce5d7a2a2ca4 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -20,17 +20,17 @@ package net.ktnx.mobileledger.ui;
 import android.app.Dialog;
 import android.os.Bundle;
 import android.widget.CalendarView;
 import android.app.Dialog;
 import android.os.Bundle;
 import android.widget.CalendarView;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatDialogFragment;
 
 import net.ktnx.mobileledger.R;
 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;
 import java.util.GregorianCalendar;
 
 import java.util.Calendar;
 import java.util.GregorianCalendar;
-import java.util.Objects;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -39,25 +39,62 @@ public class DatePickerFragment extends AppCompatDialogFragment
     static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
     static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
     static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
     static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
     static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
     static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
+    private final Calendar presentDate = GregorianCalendar.getInstance();
     private DatePickedListener onDatePickedListener;
     private DatePickedListener onDatePickedListener;
-    private MobileLedgerProfile.FutureDates futureDates = MobileLedgerProfile.FutureDates.None;
-    public MobileLedgerProfile.FutureDates getFutureDates() {
-        return futureDates;
+    private long minDate = 0;
+    private long maxDate = Long.MAX_VALUE;
+    public void setDateRange(@Nullable SimpleDate minDate, @Nullable SimpleDate maxDate) {
+        if (minDate == null)
+            this.minDate = 0;
+        else
+            this.minDate = minDate.toDate().getTime();
+
+        if (maxDate == null)
+            this.maxDate = Long.MAX_VALUE;
+        else
+            this.maxDate = maxDate.toDate().getTime();
     }
     }
-    public void setFutureDates(MobileLedgerProfile.FutureDates futureDates) {
-        this.futureDates = futureDates;
+    public void setFutureDates(FutureDates futureDates) {
+        if (futureDates == FutureDates.All) {
+            maxDate = Long.MAX_VALUE;
+        }
+        else {
+            final Calendar dateLimit = GregorianCalendar.getInstance();
+            switch (futureDates) {
+                case None:
+                    // already there
+                    break;
+                case OneWeek:
+                    dateLimit.add(Calendar.DAY_OF_MONTH, 7);
+                    break;
+                case TwoWeeks:
+                    dateLimit.add(Calendar.DAY_OF_MONTH, 14);
+                    break;
+                case OneMonth:
+                    dateLimit.add(Calendar.MONTH, 1);
+                    break;
+                case TwoMonths:
+                    dateLimit.add(Calendar.MONTH, 2);
+                    break;
+                case ThreeMonths:
+                    dateLimit.add(Calendar.MONTH, 3);
+                    break;
+                case SixMonths:
+                    dateLimit.add(Calendar.MONTH, 6);
+                    break;
+                case OneYear:
+                    dateLimit.add(Calendar.YEAR, 1);
+                    break;
+            }
+            maxDate = dateLimit.getTime()
+                               .getTime();
+        }
     }
     }
-    @NonNull
-    @Override
-    public Dialog onCreateDialog(Bundle savedInstanceState) {
-        final Calendar c = GregorianCalendar.getInstance();
-        int year = c.get(GregorianCalendar.YEAR);
-        int month = c.get(GregorianCalendar.MONTH);
-        int day = c.get(GregorianCalendar.DAY_OF_MONTH);
-        TextView date = Objects.requireNonNull(getActivity())
-                               .findViewById(R.id.new_transaction_date);
-
-        CharSequence present = date.getText();
+    public void setCurrentDateFromText(CharSequence present) {
+        final Calendar now = GregorianCalendar.getInstance();
+        int year = now.get(GregorianCalendar.YEAR);
+        int month = now.get(GregorianCalendar.MONTH);
+        int day = now.get(GregorianCalendar.DAY_OF_MONTH);
 
         Matcher m = reYMD.matcher(present);
         if (m.matches()) {
 
         Matcher m = reYMD.matcher(present);
         if (m.matches()) {
@@ -79,42 +116,20 @@ public class DatePickerFragment extends AppCompatDialogFragment
             }
         }
 
             }
         }
 
-        c.set(year, month, day);
-
-        Dialog dpd = new Dialog(Objects.requireNonNull(getActivity()));
+        presentDate.set(year, month, day);
+    }
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        Dialog dpd = new Dialog(requireActivity());
         dpd.setContentView(R.layout.date_picker_view);
         dpd.setTitle(null);
         CalendarView cv = dpd.findViewById(R.id.calendarView);
         dpd.setContentView(R.layout.date_picker_view);
         dpd.setTitle(null);
         CalendarView cv = dpd.findViewById(R.id.calendarView);
-        cv.setDate(c.getTime()
-                    .getTime());
+        cv.setDate(presentDate.getTime()
+                              .getTime());
 
 
-        if (futureDates == MobileLedgerProfile.FutureDates.All) {
-            cv.setMaxDate(Long.MAX_VALUE);
-        }
-        else {
-            switch (futureDates) {
-                case None:
-                    // already there
-                    break;
-                case OneMonth:
-                    c.add(Calendar.MONTH, 1);
-                    break;
-                case TwoMonths:
-                    c.add(Calendar.MONTH, 2);
-                    break;
-                case ThreeMonths:
-                    c.add(Calendar.MONTH, 3);
-                    break;
-                case SixMonths:
-                    c.add(Calendar.MONTH, 6);
-                    break;
-                case OneYear:
-                    c.add(Calendar.YEAR, 1);
-                    break;
-            }
-            cv.setMaxDate(c.getTime()
-                           .getTime());
-        }
+        cv.setMinDate(minDate);
+        cv.setMaxDate(maxDate);
 
         cv.setOnDateChangeListener(this);
 
 
         cv.setOnDateChangeListener(this);
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/EditTextWithClear.java b/app/src/main/java/net/ktnx/mobileledger/ui/EditTextWithClear.java
new file mode 100644 (file)
index 0000000..e39f745
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import net.ktnx.mobileledger.R;
+
+public final class EditTextWithClear extends androidx.appcompat.widget.AppCompatEditText {
+    private boolean hadText = false;
+
+    public EditTextWithClear(Context context) {
+        super(context);
+    }
+    public EditTextWithClear(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+    public EditTextWithClear(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        if (focused) {
+            final Editable text = getText();
+            if ((text != null) && (text.length() > 0)) {
+                showClearDrawable();
+            }
+        }
+        else {
+            hideClearDrawable();
+        }
+
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+    }
+    private void hideClearDrawable() {
+        setCompoundDrawablesRelative(null, null, null, null);
+    }
+    private void showClearDrawable() {
+        setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_clear_accent_24dp, 0);
+    }
+    @Override
+    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+        final boolean hasText = text.length() > 0;
+
+        if (hasFocus()) {
+            if (hadText && !hasText)
+                hideClearDrawable();
+            if (!hadText && hasText)
+                showClearDrawable();
+        }
+
+        hadText = hasText;
+
+        super.onTextChanged(text, start, lengthBefore, lengthAfter);
+    }
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (event.getAction() == MotionEvent.ACTION_UP) {
+            final Editable text = getText();
+            if ((text != null) && (text.length() > 0)) {
+                boolean clearClicked = false;
+                final float x = event.getX();
+                final int vw = getWidth();
+                // start, top, end, bottom (end == 2)
+                Drawable dwb = getCompoundDrawablesRelative()[2];
+                if (dwb != null) {
+                    final int dw = dwb.getBounds()
+                                      .width();
+                    if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
+                        if ((x > vw - dw))
+                            clearClicked = true;
+                    }
+                    else {
+                        if (x < vw - dw)
+                            clearClicked = true;
+                    }
+                    if (clearClicked) {
+                        setText("");
+                        requestFocus();
+                        performClick();
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return super.onTouchEvent(event);
+    }
+    @Override
+    public boolean performClick() {
+        return super.performClick();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java b/app/src/main/java/net/ktnx/mobileledger/ui/FabManager.java
new file mode 100644 (file)
index 0000000..bbacc3e
--- /dev/null
@@ -0,0 +1,201 @@
+/*
+ * 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();
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/HelpDialog.java b/app/src/main/java/net/ktnx/mobileledger/ui/HelpDialog.java
new file mode 100644 (file)
index 0000000..2a2bf03
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * 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());
+    }
+}
index c97741a9b1f4e98a4559c4b709a53f69b107d360..71df5bb98f75a98012761b74f8ceed13dea237d9 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -28,29 +28,28 @@ import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
 
 import android.view.MotionEvent;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.DimensionUtils;
 
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.DimensionUtils;
 
-import androidx.annotation.Nullable;
+import java.util.Locale;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class HueRing extends View {
     public static final int hueStepDegrees = 5;
     private Paint ringPaint, initialPaint, currentPaint, markerPaint;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class HueRing extends View {
     public static final int hueStepDegrees = 5;
     private Paint ringPaint, initialPaint, currentPaint, markerPaint;
-    private int centerX, centerY;
-    private int diameter;
+    private int center;
     private int padding;
     private int initialHueDegrees;
     private int color, hueDegrees;
     private float outerR;
     private float innerR;
     private float bandWidth;
     private int padding;
     private int initialHueDegrees;
     private int color, hueDegrees;
     private float outerR;
     private float innerR;
     private float bandWidth;
-    private float ringR;
-    private float innerDiameter;
     private float centerR;
     private float centerR;
-    private RectF centerRect;
-    private RectF ringRect;
+    private final RectF centerRect = new RectF();
+    private final RectF ringRect = new RectF();
     private int markerOverflow;
     private int markerStrokeWidth;
     public HueRing(Context context, @Nullable AttributeSet attrs) {
     private int markerOverflow;
     private int markerStrokeWidth;
     public HueRing(Context context, @Nullable AttributeSet attrs) {
@@ -114,13 +113,16 @@ public class HueRing extends View {
         return hueDegrees;
     }
     public void setHue(int hueDegrees) {
         return hueDegrees;
     }
     public void setHue(int hueDegrees) {
-        if (hueDegrees == -1) hueDegrees = Colors.DEFAULT_HUE_DEG;
+        if (hueDegrees == -1)
+            hueDegrees = Colors.DEFAULT_HUE_DEG;
 
         if (hueDegrees != Colors.DEFAULT_HUE_DEG) {
             // round to 15 degrees
             int rem = hueDegrees % hueStepDegrees;
 
         if (hueDegrees != Colors.DEFAULT_HUE_DEG) {
             // round to 15 degrees
             int rem = hueDegrees % hueStepDegrees;
-            if (rem < (hueStepDegrees / 2)) hueDegrees -= rem;
-            else hueDegrees += hueStepDegrees - rem;
+            if (rem < (hueStepDegrees / 2))
+                hueDegrees -= rem;
+            else
+                hueDegrees += hueStepDegrees - rem;
         }
 
         this.hueDegrees = hueDegrees;
         }
 
         this.hueDegrees = hueDegrees;
@@ -161,8 +163,8 @@ public class HueRing extends View {
 //        p.arcTo(-innerEdge, -innerEdge, innerEdge, innerEdge, -hueStepDegrees / 2f,
 //                hueStepDegrees, true);
 //        p.lineTo(outerEdge * cr, outerEdge * sr);
 //        p.arcTo(-innerEdge, -innerEdge, innerEdge, innerEdge, -hueStepDegrees / 2f,
 //                hueStepDegrees, true);
 //        p.lineTo(outerEdge * cr, outerEdge * sr);
-        p.arcTo(-outerEdge, -outerEdge, outerEdge, outerEdge, hueStepDegrees / 2f,
-                -hueStepDegrees, false);
+        p.arcTo(-outerEdge, -outerEdge, outerEdge, outerEdge, hueStepDegrees / 2f, -hueStepDegrees,
+                false);
 //        p.close();
         canvas.save();
         canvas.translate(center, center);
 //        p.close();
         canvas.save();
         canvas.translate(center, center);
@@ -177,6 +179,7 @@ public class HueRing extends View {
         int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
         int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
 
         int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
         int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
 
+        int diameter;
         if ((widthMode == MeasureSpec.AT_MOST) && (heightMode == MeasureSpec.AT_MOST)) {
             diameter = Math.min(widthSize, heightSize);
         }
         if ((widthMode == MeasureSpec.AT_MOST) && (heightMode == MeasureSpec.AT_MOST)) {
             diameter = Math.min(widthSize, heightSize);
         }
@@ -191,26 +194,29 @@ public class HueRing extends View {
 //                getContext().getResources().getDimension(R.dimen.activity_horizontal_margin)) / 2;
         diameter -= 2 * padding;
         outerR = diameter / 2f;
 //                getContext().getResources().getDimension(R.dimen.activity_horizontal_margin)) / 2;
         diameter -= 2 * padding;
         outerR = diameter / 2f;
-        centerX = padding + (int) outerR;
-        centerY = centerX;
+        center = padding + (int) outerR;
 
         bandWidth = diameter / 3.5f;
 
         bandWidth = diameter / 3.5f;
-        ringR = outerR - bandWidth / 2f;
+        float ringR = outerR - bandWidth / 2f;
         innerR = outerR - bandWidth;
 
         innerR = outerR - bandWidth;
 
-        ringRect = new RectF(-ringR, -ringR, ringR, ringR);
+        ringRect.set(-ringR, -ringR, ringR, ringR);
 
 
-        innerDiameter = diameter - 2 * bandWidth;
+        float innerDiameter = diameter - 2 * bandWidth;
         centerR = innerDiameter * 0.5f;
         centerR = innerDiameter * 0.5f;
-        centerRect = new RectF(-centerR, -centerR, centerR, centerR);
+        centerRect.set(-centerR, -centerR, centerR, centerR);
+    }
+    @Override
+    public boolean performClick() {
+        return super.performClick();
     }
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
             case MotionEvent.ACTION_MOVE:
     }
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         switch (event.getAction()) {
             case MotionEvent.ACTION_DOWN:
             case MotionEvent.ACTION_MOVE:
-                float x = event.getX() - centerX;
-                float y = event.getY() - centerY;
+                float x = event.getX() - center;
+                float y = event.getY() - center;
 
                 float dist = (float) Math.hypot(x, y);
 
 
                 float dist = (float) Math.hypot(x, y);
 
@@ -224,18 +230,22 @@ public class HueRing extends View {
                 float angleRad = (float) Math.atan2(y, x);
                 // angleRad is [-𝜋; +𝜋]
                 float hue = (float) (angleRad / (2 * Math.PI));
                 float angleRad = (float) Math.atan2(y, x);
                 // angleRad is [-𝜋; +𝜋]
                 float hue = (float) (angleRad / (2 * Math.PI));
-                if (hue < 0) hue += 1;
-                debug("TMP",
-                        String.format("x=%1.3f, y=%1.3f, angle=%1.3frad, hueDegrees=%1.3f", x, y,
-                                angleRad, hue));
+                if (hue < 0)
+                    hue += 1;
+                debug("TMP", String.format(Locale.US,
+                        "x=%1.3f, y=%1.3f, angle=%1.3f rad, hueDegrees=%1.3f", x, y, angleRad,
+                        hue));
                 setHue(hue);
                 break;
                 setHue(hue);
                 break;
+            case MotionEvent.ACTION_UP:
+                performClick();
+                break;
         }
         }
-
         return true;
     }
     public void setInitialHue(int initialHue) {
         return true;
     }
     public void setInitialHue(int initialHue) {
-        if (initialHue == -1) initialHue = Colors.DEFAULT_HUE_DEG;
+        if (initialHue == -1)
+            initialHue = Colors.DEFAULT_HUE_DEG;
         this.initialHueDegrees = initialHue;
         this.initialPaint.setColor(Colors.getPrimaryColorForHue(initialHue));
         invalidate();
         this.initialHueDegrees = initialHue;
         this.initialPaint.setColor(Colors.getPrimaryColorForHue(initialHue));
         invalidate();
index 9dc637317e4302b926f56c69a481e355c0d584e1..b10c1e025f16bd91d26d40a6d3a7775230406fff 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -21,14 +21,14 @@ import android.app.Dialog;
 import android.content.Context;
 import android.os.Bundle;
 
 import android.content.Context;
 import android.os.Bundle;
 
+import androidx.annotation.NonNull;
+
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.utils.Colors;
 
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.utils.Colors;
 
-import androidx.annotation.NonNull;
-
 public class HueRingDialog extends Dialog {
     private final int currentHue;
 public class HueRingDialog extends Dialog {
     private final int currentHue;
-    private int initialHue;
+    private final int initialHue;
     private HueRing hueRing;
     private HueSelectedListener listener;
 
     private HueRing hueRing;
     private HueSelectedListener listener;
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/MainModel.java
new file mode 100644 (file)
index 0000000..a8b957d
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
+import net.ktnx.mobileledger.async.TransactionAccumulator;
+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.TransactionListItem;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.SimpleDate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class MainModel extends ViewModel {
+    public final MutableLiveData<Integer> foundTransactionItemIndex = new MutableLiveData<>(null);
+    private final MutableLiveData<Boolean> updatingFlag = new MutableLiveData<>(false);
+    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<String> updateError = new MutableLiveData<>();
+    private SimpleDate firstTransactionDate;
+    private SimpleDate lastTransactionDate;
+    transient private RetrieveTransactionsTask retrieveTransactionsTask;
+    private TransactionsDisplayedFilter displayedTransactionsUpdater;
+    public LiveData<Boolean> getUpdatingFlag() {
+        return updatingFlag;
+    }
+    public LiveData<String> getUpdateError() {
+        return updateError;
+    }
+    public LiveData<List<TransactionListItem>> getDisplayedTransactions() {
+        return displayedTransactions;
+    }
+    public void setDisplayedTransactions(List<TransactionListItem> list, int transactionCount) {
+        displayedTransactions.postValue(list);
+        Data.lastUpdateTransactionCount.postValue(transactionCount);
+    }
+    public SimpleDate getFirstTransactionDate() {
+        return firstTransactionDate;
+    }
+    public void setFirstTransactionDate(SimpleDate earliestDate) {
+        this.firstTransactionDate = earliestDate;
+    }
+    public MutableLiveData<Boolean> getShowZeroBalanceAccounts() {return showZeroBalanceAccounts;}
+    public MutableLiveData<String> getAccountFilter() {
+        return accountFilter;
+    }
+    public SimpleDate getLastTransactionDate() {
+        return lastTransactionDate;
+    }
+    public void setLastTransactionDate(SimpleDate latestDate) {
+        this.lastTransactionDate = latestDate;
+    }
+    public synchronized void scheduleTransactionListRetrieval() {
+        if (retrieveTransactionsTask != null) {
+            Logger.debug("db", "Ignoring request for transaction retrieval - already active");
+            return;
+        }
+        Profile profile = Data.getProfile();
+        assert profile != null;
+
+        retrieveTransactionsTask = new RetrieveTransactionsTask(profile);
+        Logger.debug("db", "Created a background transaction retrieval task");
+
+        retrieveTransactionsTask.start();
+    }
+    public synchronized void stopTransactionsRetrieval() {
+        if (retrieveTransactionsTask != null)
+            retrieveTransactionsTask.interrupt();
+        else
+            Data.backgroundTaskProgress.setValue(null);
+    }
+    public void transactionRetrievalDone() {
+        retrieveTransactionsTask = null;
+    }
+    synchronized public void updateDisplayedTransactionsFromWeb(List<LedgerTransaction> list) {
+        if (displayedTransactionsUpdater != null) {
+            displayedTransactionsUpdater.interrupt();
+        }
+        displayedTransactionsUpdater = new TransactionsDisplayedFilter(this, list);
+        displayedTransactionsUpdater.start();
+    }
+    public void clearUpdateError() {
+        updateError.postValue(null);
+    }
+    public void clearTransactions() {
+        displayedTransactions.setValue(new ArrayList<>());
+    }
+
+    static class TransactionsDisplayedFilter extends Thread {
+        private final MainModel model;
+        private final List<LedgerTransaction> list;
+        TransactionsDisplayedFilter(MainModel model, List<LedgerTransaction> list) {
+            this.model = model;
+            this.list = list;
+        }
+        @Override
+        public void run() {
+            List<LedgerAccount> newDisplayed = new ArrayList<>();
+            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(accNameFilter, accNameFilter);
+            for (LedgerTransaction tr : list) {
+                if (isInterrupted()) {
+                    return;
+                }
+
+                if (accNameFilter == null || tr.hasAccountNamedLike(accNameFilter)) {
+                    acc.put(tr, tr.getDate());
+                }
+            }
+
+            if (isInterrupted())
+                return;
+
+            acc.publishResults(model);
+            Logger.debug("dFilter", "transaction list updated");
+        }
+    }
+}
index d53cc6f0a98684df0b65df71d31282e7a0b80f3b..658ce66b9b8b13252787c5539f3ff65b7384a13c 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.ui;
 
-import android.view.MotionEvent;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+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.ui.activity.MainActivity;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListAdapter;
 import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.DimensionUtils;
-
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 
 
-public class MobileLedgerListFragment extends Fragment {
-    public SwipeRefreshLayout swiper;
+public abstract class MobileLedgerListFragment extends Fragment {
     public TransactionListAdapter modelAdapter;
     public TransactionListAdapter modelAdapter;
-    protected MainActivity mActivity;
-    protected RecyclerView root;
+    public abstract SwipeRefreshLayout getRefreshLayout();
+    @NonNull
+    public MainActivity getMainActivity() {
+        return (MainActivity) requireActivity();
+    }
     protected void themeChanged(Integer counter) {
     protected void themeChanged(Integer counter) {
-        swiper.setColorSchemeColors(Colors.getColors());
+        getRefreshLayout().setColorSchemeColors(Colors.getSwipeCircleColors());
     }
     public void onBackgroundTaskRunningChanged(Boolean isRunning) {
     }
     public void onBackgroundTaskRunningChanged(Boolean isRunning) {
-        if (mActivity == null) return;
-        if (swiper == null) return;
-        swiper.setRefreshing(isRunning);
-    }
-    protected void manageFabOnScroll() {
-        int triggerPixels = DimensionUtils.dp2px(mActivity, 30f);
-        root.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
-            private float upAnchor = -1;
-            private float downAnchor = -1;
-            private float lastY;
-            @Override
-            public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
-                switch (e.getActionMasked()) {
-                    case MotionEvent.ACTION_DOWN:
-                        lastY = upAnchor = downAnchor = 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;
-
-                            mActivity.fabShouldShow();
-                        }
-                        else {
-                            // swipe up
-                            downAnchor = lastY;
-
-                            if (currentY < upAnchor - triggerPixels) mActivity.fabHide();
-                        }
-
-                        lastY = currentY;
-
-                        break;
-                }
-                return false;
-            }
-            @Override
-            public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
-            }
-            @Override
-            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
-            }
-        });
+        if (getActivity() == null)
+            return;
+        SwipeRefreshLayout l = getRefreshLayout();
+        if (l == null)
+            return;
+        l.setRefreshing(isRunning);
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencyLongClickListener.java
new file mode 100644 (file)
index 0000000..e464e73
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * 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 OnCurrencyLongClickListener {
+    void onCurrencyLongClick(String item);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnCurrencySelectedListener.java
new file mode 100644 (file)
index 0000000..94e417e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * 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 OnCurrencySelectedListener {
+    void onCurrencySelected(String item);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java b/app/src/main/java/net/ktnx/mobileledger/ui/OnSourceSelectedListener.java
new file mode 100644 (file)
index 0000000..a294adb
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/QR.java b/app/src/main/java/net/ktnx/mobileledger/ui/QR.java
new file mode 100644 (file)
index 0000000..3250039
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/QRScanCapableFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/QRScanCapableFragment.java
new file mode 100644 (file)
index 0000000..a23a402
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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);
+        });
+    }
+}
index df36690742fa6c424266544d7f6abb7a504da711..2a266b937940b66b6aa39554db472b532e726822 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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.content.Context;
 package net.ktnx.mobileledger.ui;
 
 import android.content.Context;
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.View;
 
 import android.view.GestureDetector;
 import android.view.MotionEvent;
 import android.view.View;
 
-public class RecyclerItemListener implements OnItemTouchListener {
-    private RecyclerTouchListener listener;
-    private GestureDetector gd;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener;
 
 
-    public interface RecyclerTouchListener {
-        void onClickItem(View v, int position);
-        void onLongClickItem(View v, int position);
-    }
+public class RecyclerItemListener implements OnItemTouchListener {
+    private final GestureDetector gd;
 
     public RecyclerItemListener(Context ctx, RecyclerView rv, RecyclerTouchListener listener) {
 
     public RecyclerItemListener(Context ctx, RecyclerView rv, RecyclerTouchListener listener) {
-        this.listener = listener;
-        this.gd = new GestureDetector(
-                ctx, new GestureDetector.SimpleOnGestureListener() {
+        this.gd = new GestureDetector(ctx, new GestureDetector.SimpleOnGestureListener() {
             @Override
             public void onLongPress(MotionEvent e) {
                 View v = rv.findChildViewUnder(e.getX(), e.getY());
             @Override
             public void onLongPress(MotionEvent e) {
                 View v = rv.findChildViewUnder(e.getX(), e.getY());
@@ -50,24 +43,26 @@ public class RecyclerItemListener implements OnItemTouchListener {
                 listener.onClickItem(v, rv.getChildAdapterPosition(v));
                 return true;
             }
                 listener.onClickItem(v, rv.getChildAdapterPosition(v));
                 return true;
             }
-        }
-        );
+        });
     }
     }
-
     @Override
     public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
                                          @NonNull MotionEvent motionEvent) {
         View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
         return (v != null) && gd.onTouchEvent(motionEvent);
     }
     @Override
     public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
                                          @NonNull MotionEvent motionEvent) {
         View v = recyclerView.findChildViewUnder(motionEvent.getX(), motionEvent.getY());
         return (v != null) && gd.onTouchEvent(motionEvent);
     }
-
     @Override
     public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
 
     }
     @Override
     public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
 
     }
-
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean b) {
 
     }
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean b) {
 
     }
+
+    public interface RecyclerTouchListener {
+        void onClickItem(View v, int position);
+
+        void onLongClickItem(View v, int position);
+    }
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorFragment.java
new file mode 100644 (file)
index 0000000..97c8878
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorModel.java
new file mode 100644 (file)
index 0000000..46f5885
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/TemplateDetailSourceSelectorRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..060bf13
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * 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());
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/TextViewClearHelper.java b/app/src/main/java/net/ktnx/mobileledger/ui/TextViewClearHelper.java
new file mode 100644 (file)
index 0000000..e6e7793
--- /dev/null
@@ -0,0 +1,135 @@
+/*
+ * 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;
+
+import android.annotation.SuppressLint;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.EditText;
+
+import net.ktnx.mobileledger.R;
+
+public class TextViewClearHelper {
+    private boolean hadText = false;
+    private boolean hasFocus = false;
+    private EditText view;
+    private TextWatcher textWatcher;
+    private View.OnFocusChangeListener prevOnFocusChangeListener;
+    public void detachFromView() {
+        if (view == null)
+            return;
+        view.removeTextChangedListener(textWatcher);
+        prevOnFocusChangeListener = null;
+        textWatcher = null;
+        hasFocus = false;
+        hadText = false;
+        view = null;
+    }
+    @SuppressLint("ClickableViewAccessibility")
+    public void attachToTextView(EditText view) {
+        if (this.view != null)
+            detachFromView();
+        this.view = view;
+        textWatcher = 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) {
+                boolean hasText = s.length() > 0;
+
+                if (hasFocus) {
+                    if (hadText && !hasText)
+                        hideClearDrawable();
+                    if (!hadText && hasText)
+                        showClearDrawable();
+                }
+
+                hadText = hasText;
+
+            }
+        };
+        view.addTextChangedListener(textWatcher);
+        prevOnFocusChangeListener = view.getOnFocusChangeListener();
+        view.setOnFocusChangeListener((v, hasFocus) -> {
+            if (hasFocus) {
+                if (view.getText()
+                        .length() > 0)
+                {
+                    showClearDrawable();
+                }
+            }
+            else {
+                hideClearDrawable();
+            }
+
+            this.hasFocus = hasFocus;
+
+            if (prevOnFocusChangeListener != null)
+                prevOnFocusChangeListener.onFocusChange(v, hasFocus);
+        });
+
+        view.setOnTouchListener((v, event) -> this.onTouchEvent(view, event));
+    }
+    private void hideClearDrawable() {
+        view.setCompoundDrawablesRelative(null, null, null, null);
+    }
+    private void showClearDrawable() {
+        view.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_clear_accent_24dp,
+                0);
+    }
+    public boolean onTouchEvent(EditText view, MotionEvent event) {
+        if ((event.getAction() == MotionEvent.ACTION_UP) && (view.getText()
+                                                                 .length() > 0))
+        {
+            boolean clearClicked = false;
+            final float x = event.getX();
+            final int vw = view.getWidth();
+            // start, top, end, bottom (end == 2)
+            Drawable dwb = view.getCompoundDrawablesRelative()[2];
+            if (dwb != null) {
+                final int dw = dwb.getBounds()
+                                  .width();
+                if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
+                    if ((x > vw - dw))
+                        clearClicked = true;
+                }
+                else {
+                    if (x < vw - dw)
+                        clearClicked = true;
+                }
+                if (clearClicked) {
+                    view.setText("");
+                    view.requestFocus();
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+}
index 3ceccc15748f566394e9181b369d21677215bc2b..dcc16f3678ed3885605d4b85de77653f1f957b9c 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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.content.res.Resources;
-import android.graphics.Typeface;
-import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.CheckBox;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.ui.activity.MainActivity;
-import net.ktnx.mobileledger.utils.LockHolder;
 
 import androidx.annotation.NonNull;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.constraintlayout.widget.ConstraintLayout;
 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 androidx.recyclerview.widget.RecyclerView;
 
-public class AccountSummaryAdapter
-        extends RecyclerView.Adapter<AccountSummaryAdapter.LedgerRowHolder> {
+import net.ktnx.mobileledger.R;
+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.LedgerAccount;
+import net.ktnx.mobileledger.ui.activity.MainActivity;
+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;
+
+public class AccountSummaryAdapter extends RecyclerView.Adapter<AccountSummaryAdapter.RowHolder> {
     public static final int AMOUNT_LIMIT = 3;
     public static final int AMOUNT_LIMIT = 3;
-    private boolean selectionActive;
+    private static final int ITEM_TYPE_HEADER = 1;
+    private static final int ITEM_TYPE_ACCOUNT = 2;
+    private final AsyncListDiffer<AccountListItem> listDiffer;
 
     AccountSummaryAdapter() {
 
     AccountSummaryAdapter() {
-        this.selectionActive = false;
-    }
+        setHasStableIds(true);
 
 
-    public void onBindViewHolder(@NonNull LedgerRowHolder holder, int position) {
-        try (LockHolder lh = Data.accounts.lockForReading()) {
-            if (position < Data.accounts.size()) {
-                LedgerAccount acc = Data.accounts.get(position);
-                Context ctx = holder.row.getContext();
-                Resources rm = ctx.getResources();
-
-                holder.row.setTag(acc);
-                holder.row.setVisibility(View.VISIBLE);
-                holder.vTrailer.setVisibility(View.GONE);
-                holder.tvAccountName.setText(acc.getShortName());
-                ConstraintLayout.LayoutParams lp =
-                        (ConstraintLayout.LayoutParams) holder.tvAccountName.getLayoutParams();
-                lp.setMarginStart(
-                        acc.getLevel() * rm.getDimensionPixelSize(R.dimen.thumb_row_height) / 2);
-                holder.expanderContainer
-                        .setVisibility(acc.hasSubAccounts() ? View.VISIBLE : View.INVISIBLE);
-                holder.expanderContainer.setRotation(acc.isExpanded() ? 0 : 180);
-                int amounts = acc.getAmountCount();
-                if ((amounts > AMOUNT_LIMIT) && !acc.amountsExpanded()) {
-                    holder.tvAccountAmounts.setText(acc.getAmountsString(AMOUNT_LIMIT));
-                    holder.accountExpanderContainer.setVisibility(View.VISIBLE);
-                }
-                else {
-                    holder.tvAccountAmounts.setText(acc.getAmountsString());
-                    holder.accountExpanderContainer.setVisibility(View.GONE);
-                }
+        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<AccountListItem>() {
+            @Nullable
+            @Override
+            public Object getChangePayload(@NonNull AccountListItem oldItem,
+                                           @NonNull AccountListItem newItem) {
+                Change changes = new Change();
 
 
-                if (acc.isHiddenByStar()) {
-                    holder.tvAccountName.setTypeface(null, Typeface.ITALIC);
-                    holder.tvAccountAmounts.setTypeface(null, Typeface.ITALIC);
-                }
-                else {
-                    holder.tvAccountName.setTypeface(null, Typeface.NORMAL);
-                    holder.tvAccountAmounts.setTypeface(null, Typeface.NORMAL);
-                }
+                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);
 
 
-                holder.selectionCb.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
-                holder.selectionCb.setChecked(!acc.isHiddenByStarToBe());
+                if (oldAcc.amountsExpanded() != newAcc.amountsExpanded())
+                    changes.add(Change.EXPANDED_AMOUNTS);
 
 
-                holder.row.setTag(R.id.POS, position);
+                if (!oldAcc.getAmountsString()
+                           .equals(newAcc.getAmountsString()))
+                    changes.add(Change.AMOUNTS);
+
+                return changes.toPayload();
             }
             }
-            else {
-                holder.vTrailer.setVisibility(View.VISIBLE);
-                holder.row.setVisibility(View.GONE);
+            @Override
+            public boolean areItemsTheSame(@NotNull AccountListItem oldItem,
+                                           @NotNull AccountListItem newItem) {
+                final AccountListItem.Type oldType = oldItem.getType();
+                final AccountListItem.Type newType = newItem.getType();
+                if (oldType != newType)
+                    return false;
+                if (oldType == AccountListItem.Type.HEADER)
+                    return true;
+
+                return oldItem.toAccount()
+                              .getAccount()
+                              .getId() == newItem.toAccount()
+                                                 .getAccount()
+                                                 .getId();
             }
             }
-        }
+            @Override
+            public boolean areContentsTheSame(@NotNull AccountListItem oldItem,
+                                              @NotNull AccountListItem newItem) {
+                return oldItem.sameContent(newItem);
+            }
+        });
+    }
+    @Override
+    public long getItemId(int position) {
+        if (position == 0)
+            return 0;
+        return listDiffer.getCurrentList()
+                         .get(position)
+                         .toAccount()
+                         .getAccount()
+                         .getId();
     }
     }
-
-    @NonNull
     @Override
     @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() {
     @Override
     public int getItemCount() {
-        return Data.accounts.size();
+        return listDiffer.getCurrentList()
+                         .size();
     }
     }
-    public void startSelection() {
-        try (LockHolder lh = Data.accounts.lockForWriting()) {
-            for (int i = 0; i < Data.accounts.size(); i++) {
-                LedgerAccount acc = Data.accounts.get(i);
-                acc.setHiddenByStarToBe(acc.isHiddenByStar());
-            }
-            this.selectionActive = true;
-            lh.downgrade();
-            notifyDataSetChanged();
-        }
+    @Override
+    public int getItemViewType(int position) {
+        return (position == 0) ? ITEM_TYPE_HEADER : ITEM_TYPE_ACCOUNT;
     }
     }
-
-    public void stopSelection() {
-        this.selectionActive = false;
-        notifyDataSetChanged();
+    public void setAccounts(List<AccountListItem> newList) {
+        Misc.onMainThread(() -> listDiffer.submitList(newList));
     }
     }
-
-    public boolean isSelectionActive() {
-        return selectionActive;
+    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;
+        }
     }
 
     }
 
-    public void selectItem(int position) {
-        try (LockHolder lh = Data.accounts.lockForWriting()) {
-            LedgerAccount acc = Data.accounts.get(position);
-            acc.toggleHiddenToBe();
-            toggleChildrenOf(acc, acc.isHiddenByStarToBe(), position);
-            notifyItemChanged(position);
+    static abstract class RowHolder extends RecyclerView.ViewHolder {
+        public RowHolder(@NonNull View itemView) {
+            super(itemView);
         }
         }
+        public abstract void bind(AccountListItem accountListItem, @Nullable List<Object> payloads);
     }
     }
-    void toggleChildrenOf(LedgerAccount parent, boolean hiddenToBe, int parentPosition) {
-        int i = parentPosition + 1;
-        try (LockHolder lh = Data.accounts.lockForWriting()) {
-            for (int j = 0; j < Data.accounts.size(); j++) {
-                LedgerAccount acc = Data.accounts.get(j);
-                if (acc.getName().startsWith(parent.getName() + ":")) {
-                    acc.setHiddenByStarToBe(hiddenToBe);
-                    notifyItemChanged(i);
-                    toggleChildrenOf(acc, hiddenToBe, i);
-                    i++;
-                }
-            }
+
+    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);
         }
     }
 
         }
     }
 
-    class LedgerRowHolder extends RecyclerView.ViewHolder {
-        CheckBox selectionCb;
-        TextView tvAccountName, tvAccountAmounts;
-        ConstraintLayout row;
-        View vTrailer;
-        FrameLayout expanderContainer;
-        ImageView expander;
-        FrameLayout accountExpanderContainer;
-        public LedgerRowHolder(@NonNull View itemView) {
-            super(itemView);
-            this.row = itemView.findViewById(R.id.account_summary_row);
-            this.tvAccountName = itemView.findViewById(R.id.account_row_acc_name);
-            this.tvAccountAmounts = itemView.findViewById(R.id.account_row_acc_amounts);
-            this.selectionCb = itemView.findViewById(R.id.account_row_check);
-            this.vTrailer = itemView.findViewById(R.id.account_summary_trailer);
-            this.expanderContainer = itemView.findViewById(R.id.account_expander_container);
-            this.expander = itemView.findViewById(R.id.account_expander);
-            this.accountExpanderContainer =
-                    itemView.findViewById(R.id.account_row_amounts_expander_container);
-
-            expanderContainer.addOnLayoutChangeListener(
-                    (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
-                        int w = right - left;
-                        int h = bottom - top;
-                        if (h > w) {
-                            int p = (h - w) / 2;
-                            v.setPadding(0, p, 0, p);
-                        }
-                        else v.setPadding(0, 0, 0, 0);
-                    });
+    class AccountRowHolder extends AccountSummaryAdapter.RowHolder {
+        private final AccountListRowBinding b;
+        public AccountRowHolder(@NonNull AccountListRowBinding binding) {
+            super(binding.getRoot());
+            b = binding;
 
             itemView.setOnLongClickListener(this::onItemLongClick);
 
             itemView.setOnLongClickListener(this::onItemLongClick);
-            tvAccountName.setOnLongClickListener(this::onItemLongClick);
-            tvAccountAmounts.setOnLongClickListener(this::onItemLongClick);
-            expanderContainer.setOnLongClickListener(this::onItemLongClick);
-            expander.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() {
+            LedgerAccount account = getAccount();
+            if (!account.hasSubAccounts())
+                return;
+            debug("accounts", "Account expander clicked");
+
+            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() {
+            LedgerAccount account = getAccount();
+            if (account.getAmountCount() <= AMOUNT_LIMIT)
+                return;
+
+            account.toggleAmountsExpanded();
+            if (account.amountsExpanded()) {
+                b.accountRowAccAmounts.setText(account.getAmountsString());
+                b.accountRowAmountsExpanderContainer.setVisibility(View.GONE);
+            }
+            else {
+                b.accountRowAccAmounts.setText(account.getAmountsString(AMOUNT_LIMIT));
+                b.accountRowAmountsExpanderContainer.setVisibility(View.VISIBLE);
+            }
+
+            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);
         }
         private boolean onItemLongClick(View v) {
             MainActivity activity = (MainActivity) v.getContext();
             AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-            View row;
-            int id = v.getId();
-            switch (id) {
-                case R.id.account_summary_row:
-                    row = v;
-                    break;
-                case R.id.account_root:
-                    row = v.findViewById(R.id.account_summary_row);
-                    break;
-                case R.id.account_row_acc_name:
-                case R.id.account_row_acc_amounts:
-                case R.id.account_expander_container:
-                    row = (View) v.getParent();
-                    break;
-                case R.id.account_expander:
-                    row = (View) v.getParent().getParent();
-                    break;
-                default:
-                    Log.e("error", String.format("Don't know how to handle long click on id ", id));
-                    return false;
-            }
-            LedgerAccount acc = (LedgerAccount) row.findViewById(R.id.account_summary_row).getTag();
-            builder.setTitle(acc.getName());
+            final String accountName = getAccount().getName();
+            builder.setTitle(accountName);
             builder.setItems(R.array.acc_ctx_menu, (dialog, which) -> {
             builder.setItems(R.array.acc_ctx_menu, (dialog, which) -> {
-                switch (which) {
-                    case 0:
-                        // show transactions
-                        activity.showAccountTransactions(acc);
-                        break;
+                if (which == 0) {// show transactions
+                    activity.showAccountTransactions(accountName);
+                }
+                else {
+                    throw new RuntimeException(String.format("Unknown menu item id (%d)", which));
                 }
                 dialog.dismiss();
             });
             builder.show();
             return true;
         }
                 }
                 dialog.dismiss();
             });
             builder.show();
             return true;
         }
+        @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));
+
+            Resources rm = b.getRoot()
+                            .getContext()
+                            .getResources();
+
+            if (changes.has(Change.NAME))
+                b.accountRowAccName.setText(acc.getShortName());
+
+            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);
+            }
+
+            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);
+                    }
+                }
+            }
+            else {
+                b.accountExpanderContainer.setVisibility(View.GONE);
+            }
+
+            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);
+                }
+            }
+        }
     }
 }
     }
 }
index bb484929d9f221236bde6b05209cc5b07f5c25d4..807c16daf8540af877827e2729d0e848f733d3c3 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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.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 android.view.View;
 import android.view.ViewGroup;
 
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+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.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.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 org.jetbrains.annotations.NotNull;
 
 import net.ktnx.mobileledger.ui.MobileLedgerListFragment;
 import net.ktnx.mobileledger.ui.activity.MainActivity;
 import net.ktnx.mobileledger.utils.Colors;
 
 import org.jetbrains.annotations.NotNull;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.DividerItemDecoration;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 
 public class AccountSummaryFragment extends MobileLedgerListFragment {
     public AccountSummaryAdapter modelAdapter;
 
 public class AccountSummaryFragment extends MobileLedgerListFragment {
     public AccountSummaryAdapter modelAdapter;
-    /*
-        private MenuItem mShowOnlyStarred;
-        private Menu optMenu;
-    */
-    private FloatingActionButton fab;
+    private AccountSummaryFragmentBinding b;
+    private MenuItem menuShowZeroBalances;
+    private MainModel model;
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         debug("flow", "AccountSummaryFragment.onCreate()");
         setHasOptionsMenu(true);
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         debug("flow", "AccountSummaryFragment.onCreate()");
         setHasOptionsMenu(true);
-
-        Data.backgroundTasksRunning.observe(this, this::onBackgroundTaskRunningChanged);
     }
     public void onAttach(@NotNull Context context) {
         super.onAttach(context);
         debug("flow", "AccountSummaryFragment.onAttach()");
     }
     public void onAttach(@NotNull Context context) {
         super.onAttach(context);
         debug("flow", "AccountSummaryFragment.onAttach()");
-        mActivity = (MainActivity) context;
     }
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                              @Nullable Bundle savedInstanceState) {
         debug("flow", "AccountSummaryFragment.onCreateView()");
     }
     @Override
     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
     @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()");
         debug("flow", "AccountSummaryFragment.onActivityCreated()");
-        super.onActivityCreated(savedInstanceState);
+        super.onViewCreated(view, savedInstanceState);
+
+        model = new ViewModelProvider(requireActivity()).get(MainModel.class);
+
+        Data.backgroundTasksRunning.observe(this.getViewLifecycleOwner(),
+                this::onBackgroundTaskRunningChanged);
 
         modelAdapter = new AccountSummaryAdapter();
 
         modelAdapter = new AccountSummaryAdapter();
+        MainActivity mainActivity = getMainActivity();
 
 
-        mActivity.mAccountSummaryFragment = this;
-        root = mActivity.findViewById(R.id.account_root);
-        LinearLayoutManager llm = new LinearLayoutManager(mActivity);
+        LinearLayoutManager llm = new LinearLayoutManager(mainActivity);
         llm.setOrientation(RecyclerView.VERTICAL);
         llm.setOrientation(RecyclerView.VERTICAL);
-        root.setLayoutManager(llm);
-        root.setAdapter(modelAdapter);
+        b.accountRoot.setLayoutManager(llm);
+        b.accountRoot.setAdapter(modelAdapter);
         DividerItemDecoration did =
         DividerItemDecoration did =
-                new DividerItemDecoration(mActivity, DividerItemDecoration.VERTICAL);
-        root.addItemDecoration(did);
-
-        fab = mActivity.findViewById(R.id.btn_add_transaction);
+                new DividerItemDecoration(mainActivity, DividerItemDecoration.VERTICAL);
+        b.accountRoot.addItemDecoration(did);
 
 
-        mActivity.fabShouldShow();
+        mainActivity.fabShouldShow();
 
 
-        manageFabOnScroll();
+        if (mainActivity instanceof FabManager.FabHandler)
+            FabManager.handle(mainActivity, b.accountRoot);
 
 
-        swiper = mActivity.findViewById(R.id.account_swiper);
-        Colors.themeWatch.observe(this, this::themeChanged);
-        swiper.setOnRefreshListener(() -> {
+        Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
+        b.accountSwipeRefreshLayout.setOnRefreshListener(() -> {
             debug("ui", "refreshing accounts via swipe");
             debug("ui", "refreshing accounts via swipe");
-            Data.scheduleTransactionListRetrieval(mActivity);
+            model.scheduleTransactionListRetrieval();
         });
 
         });
 
-        Data.accounts.addObserver(
-                (o, arg) -> mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()));
-    }
-/*
-    void stopSelection() {
-        modelAdapter.stopSelection();
-        if (optMenu != null) {
-            optMenu.findItem(R.id.menu_acc_summary_cancel_selection).setVisible(false);
-            optMenu.findItem(R.id.menu_acc_summary_confirm_selection).setVisible(false);
-            optMenu.findItem(R.id.menu_acc_summary_only_starred).setVisible(true);
-        }
-        {
-            if (fab != null) fab.show();
-        }
-    }
-    public void onCancelAccSelection(MenuItem item) {
-        stopSelection();
-    }
-    public void onConfirmAccSelection(MenuItem item) {
-        AccountSummaryViewModel.commitSelections(mActivity);
-        stopSelection();
+        Data.observeProfile(this, profile -> onProfileChanged(profile, Boolean.TRUE.equals(
+                model.getShowZeroBalanceAccounts()
+                     .getValue())));
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
-        // Inflate the menu; this adds items to the action bar if it is present.
-        inflater.inflate(R.menu.account_summary, menu);
-        optMenu = menu;
-
-        mShowOnlyStarred = menu.findItem(R.id.menu_acc_summary_only_starred);
-        if (mShowOnlyStarred == null) throw new AssertionError();
-        MenuItem mCancelSelection = menu.findItem(R.id.menu_acc_summary_cancel_selection);
-        if (mCancelSelection == null) throw new AssertionError();
-        MenuItem mConfirmSelection = menu.findItem(R.id.menu_acc_summary_confirm_selection);
-        if (mConfirmSelection == null) throw new AssertionError();
-
-        Data.optShowOnlyStarred.addObserver((o, arg) -> {
-            boolean newValue = Data.optShowOnlyStarred.get();
-            debug("pref", String.format("pref change came (%s)", newValue ? "true" : "false"));
-            mShowOnlyStarred.setChecked(newValue);
-            update_account_table();
-        });
-
-        mShowOnlyStarred.setChecked(Data.optShowOnlyStarred.get());
+        inflater.inflate(R.menu.account_list, menu);
 
 
-        debug("menu", "Accounts: onCreateOptionsMenu called");
-
-        mShowOnlyStarred.setOnMenuItemClickListener(item -> {
-            SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mActivity);
-            SharedPreferences.Editor editor = pref.edit();
-            boolean flag = item.isChecked();
-            editor.putBoolean(PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS, !flag);
-            debug("pref",
-                    "Setting show only starred accounts pref to " + (flag ? "false" : "true"));
-            editor.apply();
+        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;
         });
 
             return true;
         });
 
-        mCancelSelection.setOnMenuItemClickListener(item -> {
-            stopSelection();
-            return true;
-        });
+        model.getShowZeroBalanceAccounts()
+             .observe(this, v -> {
+                 menuShowZeroBalances.setChecked(v);
+                 onProfileChanged(Data.getProfile(), v);
+             });
 
 
-        mConfirmSelection.setOnMenuItemClickListener(item -> {
-            AccountSummaryViewModel.commitSelections(mActivity);
-            stopSelection();
-
-            return true;
-        });
+        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 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);
+        }
     }
     }
-*/
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/account_summary/AccountSummaryViewModel.java
deleted file mode 100644 (file)
index 3e9af7b..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.account_summary;
-
-import android.content.Context;
-import android.os.AsyncTask;
-
-import net.ktnx.mobileledger.async.CommitAccountsTask;
-import net.ktnx.mobileledger.async.CommitAccountsTaskParams;
-import net.ktnx.mobileledger.async.UpdateAccountsTask;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.LedgerAccount;
-
-import java.util.ArrayList;
-
-import androidx.lifecycle.ViewModel;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class AccountSummaryViewModel extends ViewModel {
-    static void commitSelections(Context context) {
-        CAT task = new CAT();
-        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
-                new CommitAccountsTaskParams(Data.accounts, Data.optShowOnlyStarred.get()));
-    }
-    static public void scheduleAccountListReload() {
-        if (Data.profile.getValue() == null) return;
-
-        UAT task = new UAT();
-        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-
-    }
-
-    private static class UAT extends UpdateAccountsTask {
-        @Override
-        protected void onPostExecute(ArrayList<LedgerAccount> list) {
-            super.onPostExecute(list);
-            if (list != null) {
-                debug("acc", "setting updated account list");
-                Data.accounts.setList(list);
-            }
-        }
-    }
-
-    private static class CAT extends CommitAccountsTask {
-        @Override
-        protected void onPostExecute(ArrayList<LedgerAccount> list) {
-            super.onPostExecute(list);
-            if (list != null) {
-                debug("acc", "setting new account list");
-                Data.accounts.setList(list);
-            }
-        }
-    }
-}
-
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/AppCompatPreferenceActivity.java
deleted file mode 100644 (file)
index ad02cd2..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import androidx.annotation.LayoutRes;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.ActionBar;
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.appcompat.widget.Toolbar;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import net.ktnx.mobileledger.utils.Colors;
-
-/**
- * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
- * to be used with AppCompat.
- */
-public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
-
-    private AppCompatDelegate mDelegate;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        getDelegate().installViewFactory();
-        getDelegate().onCreate(savedInstanceState);
-        super.onCreate(savedInstanceState);
-        Colors.setupTheme(this);
-    }
-
-    @Override
-    protected void onPostCreate(Bundle savedInstanceState) {
-        super.onPostCreate(savedInstanceState);
-        getDelegate().onPostCreate(savedInstanceState);
-    }
-
-    public ActionBar getSupportActionBar() {
-        return getDelegate().getSupportActionBar();
-    }
-
-    public void setSupportActionBar(@Nullable Toolbar toolbar) {
-        getDelegate().setSupportActionBar(toolbar);
-    }
-
-    @NonNull
-    @Override
-    public MenuInflater getMenuInflater() {
-        return getDelegate().getMenuInflater();
-    }
-
-    @Override
-    public void setContentView(@LayoutRes int layoutResID) {
-        getDelegate().setContentView(layoutResID);
-    }
-
-    @Override
-    public void setContentView(View view) {
-        getDelegate().setContentView(view);
-    }
-
-    @Override
-    public void setContentView(View view, ViewGroup.LayoutParams params) {
-        getDelegate().setContentView(view, params);
-    }
-
-    @Override
-    public void addContentView(View view, ViewGroup.LayoutParams params) {
-        getDelegate().addContentView(view, params);
-    }
-
-    @Override
-    protected void onPostResume() {
-        super.onPostResume();
-        getDelegate().onPostResume();
-    }
-
-    @Override
-    protected void onTitleChanged(CharSequence title, int color) {
-        super.onTitleChanged(title, color);
-        getDelegate().setTitle(title);
-    }
-
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-        super.onConfigurationChanged(newConfig);
-        getDelegate().onConfigurationChanged(newConfig);
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        getDelegate().onStop();
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        getDelegate().onDestroy();
-    }
-
-    public void invalidateOptionsMenu() {
-        getDelegate().invalidateOptionsMenu();
-    }
-
-    private AppCompatDelegate getDelegate() {
-        if (mDelegate == null) {
-            mDelegate = AppCompatDelegate.create(this, null);
-        }
-        return mDelegate;
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/AsyncCrasher.java
deleted file mode 100644 (file)
index ce02ff5..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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");
-    }
-}
index 183a5e9184d84ad0ec847a507e013d17f75ff791..a134c892f856a2f0384cc0866b76928e478d9286 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -37,32 +37,29 @@ public abstract class CrashReportingActivity extends AppCompatActivity {
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
-            @Override
-            public void uncaughtException(Thread t, Throwable e) {
-                StringWriter sw = new StringWriter();
-                PrintWriter pw = new PrintWriter(sw);
+        Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
+            StringWriter sw = new StringWriter();
+            PrintWriter pw = new PrintWriter(sw);
 
 
-                try {
-                    PackageInfo pi = getApplicationContext().getPackageManager()
-                            .getPackageInfo(getPackageName(), 0);
-                    pw.format("MoLe version: %s\n", pi.versionName);
-                }
-                catch (Exception oh) {
-                    pw.print("Error getting package version:\n");
-                    oh.printStackTrace(pw);
-                    pw.print("\n");
-                }
-                pw.format("OS version: %s; API level %d\n\n", Build.VERSION.RELEASE,
-                        Build.VERSION.SDK_INT);
-                e.printStackTrace(pw);
+            try {
+                PackageInfo pi = getApplicationContext().getPackageManager()
+                                                        .getPackageInfo(getPackageName(), 0);
+                pw.format("MoLe version: %s\n", pi.versionName);
+            }
+            catch (Exception oh) {
+                pw.print("Error getting package version:\n");
+                oh.printStackTrace(pw);
+                pw.print("\n");
+            }
+            pw.format("OS version: %s; API level %d\n\n", Build.VERSION.RELEASE,
+                    Build.VERSION.SDK_INT);
+            e.printStackTrace(pw);
 
 
-                Log.e(null, sw.toString());
+            Log.e(null, sw.toString());
 
 
-                CrashReportDialogFragment df = new CrashReportDialogFragment();
-                df.setCrashReportText(sw.toString());
-                df.show(getSupportFragmentManager(), "crash_report");
-            }
+            CrashReportDialogFragment df = new CrashReportDialogFragment();
+            df.setCrashReportText(sw.toString());
+            df.show(getSupportFragmentManager(), "crash_report");
         });
         debug("crash", "Uncaught exception handler set");
     }
         });
         debug("crash", "Uncaught exception handler set");
     }
index fd6942b03b6d2a21e51d1c1d76f572128e83774f..e686d96fdb37233dd2a0ffcd372b6ebdf359ba96 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.ui.activity;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.content.pm.PackageInfo;
 import android.content.pm.ShortcutInfo;
 import android.content.pm.ShortcutManager;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
-import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Build;
 import android.os.Bundle;
+import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.View;
 import android.util.Log;
 import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewPropertyAnimator;
 import android.view.animation.AnimationUtils;
 import android.view.animation.AnimationUtils;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.TextView;
-import android.widget.Toast;
 
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.ActionBarDrawerToggle;
 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.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.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.R;
-import net.ktnx.mobileledger.async.DbOpQueue;
-import net.ktnx.mobileledger.async.RefreshDescriptionsTask;
 import net.ktnx.mobileledger.async.RetrieveTransactionsTask;
 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.Data;
-import net.ktnx.mobileledger.model.LedgerAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.account_summary.AccountSummaryAdapter;
+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.account_summary.AccountSummaryFragment;
-import net.ktnx.mobileledger.ui.account_summary.AccountSummaryViewModel;
-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.profiles.ProfilesRecyclerViewAdapter;
+import net.ktnx.mobileledger.ui.templates.TemplatesActivity;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
 import net.ktnx.mobileledger.ui.transaction_list.TransactionListFragment;
-import net.ktnx.mobileledger.ui.transaction_list.TransactionListViewModel;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.GetOptCallback;
-import net.ktnx.mobileledger.utils.LockHolder;
-import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
 
 import org.jetbrains.annotations.NotNull;
 
 
 import org.jetbrains.annotations.NotNull;
 
-import java.text.DateFormat;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Objects;
 
 
-import static net.ktnx.mobileledger.utils.Logger.debug;
+/*
+ * 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";
     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";
-    private static final String PREF_THEME_ID = "themeId";
-    public AccountSummaryFragment mAccountSummaryFragment;
-    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 SectionsPagerAdapter mSectionsPagerAdapter;
-    private ViewPager mViewPager;
-    private FloatingActionButton fab;
     private ProfilesRecyclerViewAdapter mProfileListAdapter;
     private int mCurrentPage;
     private boolean mBackMeansToAccountList = false;
     private ProfilesRecyclerViewAdapter mProfileListAdapter;
     private int mCurrentPage;
     private boolean mBackMeansToAccountList = false;
-    private Toolbar mToolbar;
     private DrawerLayout.SimpleDrawerListener drawerListener;
     private ActionBarDrawerToggle barDrawerToggle;
     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();
 
     @Override
     protected void onStart() {
         super.onStart();
 
-        mViewPager.setCurrentItem(mCurrentPage, false);
+        Logger.debug(TAG, "onStart()");
+
+        b.mainPager.setCurrentItem(mCurrentPage, false);
     }
     @Override
     protected void onSaveInstanceState(@NotNull Bundle outState) {
         super.onSaveInstanceState(outState);
     }
     @Override
     protected void onSaveInstanceState(@NotNull Bundle outState) {
         super.onSaveInstanceState(outState);
-        outState.putInt(STATE_CURRENT_PAGE, mViewPager.getCurrentItem());
-        if (Data.accountFilter.getValue() != null)
-            outState.putString(STATE_ACC_FILTER, Data.accountFilter.getValue());
+        outState.putInt(STATE_CURRENT_PAGE, b.mainPager.getCurrentItem());
+        if (mainModel.getAccountFilter()
+                     .getValue() != null)
+            outState.putString(STATE_ACC_FILTER, mainModel.getAccountFilter()
+                                                          .getValue());
     }
     @Override
     protected void onDestroy() {
         mSectionsPagerAdapter = null;
     }
     @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;
         drawerListener = null;
-        if (drawer != null) drawer.removeDrawerListener(barDrawerToggle);
+        b.drawerLayout.removeDrawerListener(barDrawerToggle);
         barDrawerToggle = null;
         barDrawerToggle = null;
-        if (mViewPager != null) mViewPager.removeOnPageChangeListener(pageChangeListener);
-        pageChangeListener = null;
+        b.mainPager.unregisterOnPageChangeCallback(pageChangeCallback);
+        pageChangeCallback = null;
         super.onDestroy();
     }
     @Override
         super.onDestroy();
     }
     @Override
-    protected void setupProfileColors() {
-        SharedPreferences prefs = getPreferences(MODE_PRIVATE);
-        int profileColor = prefs.getInt(PREF_THEME_ID, -2);
-        if (profileColor == -2) profileColor = Data.retrieveCurrentThemeIdFromDb();
-        Colors.setupTheme(this, profileColor);
-        Colors.profileThemeId = profileColor;
-        storeThemeIdInPrefs(profileColor);
-    }
-    @Override
     protected void onResume() {
         super.onResume();
         fabShouldShow();
     }
     @Override
     protected void onCreate(Bundle savedInstanceState) {
     protected void onResume() {
         super.onResume();
         fabShouldShow();
     }
     @Override
     protected void onCreate(Bundle savedInstanceState) {
+        Logger.debug(TAG, "onCreate()/entry");
         super.onCreate(savedInstanceState);
         super.onCreate(savedInstanceState);
-        debug("flow", "MainActivity.onCreate()");
-        setContentView(R.layout.activity_main);
-
-        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);
+        Logger.debug(TAG, "onCreate()/after super");
+        b = ActivityMainBinding.inflate(getLayoutInflater());
+        setContentView(b.getRoot());
+
+        mainModel = new ViewModelProvider(this).get(MainModel.class);
+
+        mSectionsPagerAdapter = new SectionsPagerAdapter(this);
 
         Bundle extra = getIntent().getBundleExtra(BUNDLE_SAVED_STATE);
 
         Bundle extra = getIntent().getBundleExtra(BUNDLE_SAVED_STATE);
-        if (extra != null && savedInstanceState == null) savedInstanceState = extra;
+        if (extra != null && savedInstanceState == null)
+            savedInstanceState = extra;
 
 
 
 
-        mToolbar = findViewById(R.id.toolbar);
-        setSupportActionBar(mToolbar);
+        setSupportActionBar(b.toolbar);
 
 
-        Data.profile.observe(this, this::onProfileChanged);
+        Data.observeProfile(this, this::onProfileChanged);
 
         Data.profiles.observe(this, this::onProfileListChanged);
 
 
         Data.profiles.observe(this, this::onProfileListChanged);
 
+        Data.backgroundTaskProgress.observe(this, this::onRetrieveProgress);
+        Data.backgroundTasksRunning.observe(this, this::onRetrieveRunningChanged);
+
         if (barDrawerToggle == null) {
         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();
 
         }
         barDrawerToggle.syncState();
 
-        TextView ver = drawer.findViewById(R.id.drawer_version_text);
-
         try {
         try {
-            PackageInfo pi =
-                    getApplicationContext().getPackageManager().getPackageInfo(getPackageName(), 0);
-            ver.setText(pi.versionName);
+            PackageInfo pi = getApplicationContext().getPackageManager()
+                                                    .getPackageInfo(getPackageName(), 0);
+            ((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();
         }
         catch (Exception e) {
             e.printStackTrace();
@@ -188,10 +184,11 @@ public class MainActivity extends ProfileThemedActivity {
 
         markDrawerItemCurrent(R.id.nav_account_summary);
 
 
         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;
                 @Override
                 public void onPageSelected(int position) {
                     mCurrentPage = position;
@@ -203,14 +200,13 @@ public class MainActivity extends ProfileThemedActivity {
                             markDrawerItemCurrent(R.id.nav_latest_transactions);
                             break;
                         default:
                             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);
                 }
             };
                     }
 
                     super.onPageSelected(position);
                 }
             };
-            mViewPager.addOnPageChangeListener(pageChangeListener);
+            b.mainPager.registerOnPageChangeCallback(pageChangeCallback);
         }
 
         mCurrentPage = 0;
         }
 
         mCurrentPage = 0;
@@ -219,50 +215,47 @@ public class MainActivity extends ProfileThemedActivity {
             if (currentPage != -1) {
                 mCurrentPage = currentPage;
             }
             if (currentPage != -1) {
                 mCurrentPage = currentPage;
             }
-            Data.accountFilter.setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
+            mainModel.getAccountFilter()
+                     .setValue(savedInstanceState.getString(STATE_ACC_FILTER, null));
         }
 
         }
 
-        Data.lastUpdateDate.observe(this, this::updateLastUpdateDisplay);
+        b.btnNoProfilesAdd.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
+        b.btnRestore.setOnClickListener(v -> BackupsActivity.start(this));
 
 
-        findViewById(R.id.btn_no_profiles_add)
-                .setOnClickListener(v -> startEditProfileActivity(null));
+        b.btnAddTransaction.setOnClickListener(this::fabNewTransactionClicked);
 
 
-        findViewById(R.id.btn_add_transaction).setOnClickListener(this::fabNewTransactionClicked);
+        b.navNewProfileButton.setOnClickListener(v -> ProfileDetailActivity.start(this, null));
 
 
-        findViewById(R.id.nav_new_profile_button)
-                .setOnClickListener(v -> startEditProfileActivity(null));
+        b.transactionListCancelDownload.setOnClickListener(this::onStopTransactionRefreshClick);
 
 
-        RecyclerView root = findViewById(R.id.nav_profile_list);
-        if (root == null)
-            throw new RuntimeException("Can't get hold on the transaction value view");
-
-        if (mProfileListAdapter == null) mProfileListAdapter = new ProfilesRecyclerViewAdapter();
-        root.setAdapter(mProfileListAdapter);
+        if (mProfileListAdapter == null)
+            mProfileListAdapter = new ProfilesRecyclerViewAdapter();
+        b.navProfileList.setAdapter(mProfileListAdapter);
 
         mProfileListAdapter.editingProfiles.observe(this, newValue -> {
             if (newValue) {
 
         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));
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
-                    profileListHeadCancel.startAnimation(
+                    b.navProfilesCancelEdit.startAnimation(
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
-                    profileListHeadAddProfile.startAnimation(
+                    b.navNewProfileButton.startAnimation(
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
                 }
             }
             else {
                             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));
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
-                    profileListHeadMore.startAnimation(
+                    b.navProfilesStartEdit.startAnimation(
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_in));
-                    profileListHeadAddProfile.startAnimation(
+                    b.navNewProfileButton.startAnimation(
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
                 }
             }
                             AnimationUtils.loadAnimation(MainActivity.this, R.anim.fade_out));
                 }
             }
@@ -270,267 +263,320 @@ public class MainActivity extends ProfileThemedActivity {
             mProfileListAdapter.notifyDataSetChanged();
         });
 
             mProfileListAdapter.notifyDataSetChanged();
         });
 
+        fabManager = new FabManager(b.btnAddTransaction);
+
         LinearLayoutManager llm = new LinearLayoutManager(this);
 
         llm.setOrientation(RecyclerView.VERTICAL);
         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() {
         if (drawerListener == null) {
             drawerListener = new DrawerLayout.SimpleDrawerListener() {
+                @Override
+                public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {
+                    if (slideOffset > 0.2)
+                        fabManager.hideFab();
+                }
                 @Override
                 public void onDrawerClosed(View drawerView) {
                     super.onDrawerClosed(drawerView);
                     mProfileListAdapter.setAnimationsEnabled(false);
                     mProfileListAdapter.editingProfiles.setValue(false);
                 @Override
                 public void onDrawerClosed(View drawerView) {
                     super.onDrawerClosed(drawerView);
                     mProfileListAdapter.setAnimationsEnabled(false);
                     mProfileListAdapter.editingProfiles.setValue(false);
+                    Data.drawerOpen.setValue(false);
+                    fabShouldShow();
                 }
                 @Override
                 public void onDrawerOpened(View drawerView) {
                     super.onDrawerOpened(drawerView);
                     mProfileListAdapter.setAnimationsEnabled(true);
                 }
                 @Override
                 public void onDrawerOpened(View drawerView) {
                     super.onDrawerOpened(drawerView);
                     mProfileListAdapter.setAnimationsEnabled(true);
+                    Data.drawerOpen.setValue(true);
+                    fabManager.hideFab();
                 }
             };
                 }
             };
-            drawer.addDrawerListener(drawerListener);
+            b.drawerLayout.addDrawerListener(drawerListener);
         }
         }
-        setupProfile();
+
+        Data.drawerOpen.observe(this, open -> {
+            if (open)
+                b.drawerLayout.open();
+            else
+                b.drawerLayout.close();
+        });
+
+        mainModel.getUpdateError()
+                 .observe(this, (error) -> {
+                     if (error == null)
+                         return;
+
+                     Snackbar.make(b.mainPager, error, Snackbar.LENGTH_INDEFINITE)
+                             .show();
+                     mainModel.clearUpdateError();
+                 });
+        Data.locale.observe(this, l -> refreshLastUpdateInfo());
+        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(Date lastUpdate) {
+    private void scheduleDataRetrievalIfStale(long lastUpdate) {
         long now = new Date().getTime();
         long now = new Date().getTime();
-        if ((lastUpdate == null) || (now > (lastUpdate.getTime() + (24 * 3600 * 1000)))) {
-            if (lastUpdate == null) debug("db::", "WEB data never fetched. scheduling a fetch");
-            else debug("db", String.format(Locale.ENGLISH,
-                    "WEB data last fetched at %1.3f and now is %1.3f. re-fetching",
-                    lastUpdate.getTime() / 1000f, now / 1000f));
-
-            Data.scheduleTransactionListRetrieval(this);
+        if ((lastUpdate == 0) || (now > (lastUpdate + (24 * 3600 * 1000)))) {
+            if (lastUpdate == 0)
+                Logger.debug("db::", "WEB data never fetched. scheduling a fetch");
+            else
+                Logger.debug("db", String.format(Locale.ENGLISH,
+                        "WEB data last fetched at %1.3f and now is %1.3f. re-fetching",
+                        lastUpdate / 1000f, now / 1000f));
+
+            mainModel.scheduleTransactionListRetrieval();
         }
     }
         }
     }
-    private void createShortcuts(List<MobileLedgerProfile> list) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return;
+    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;
         List<ShortcutInfo> shortcuts = new ArrayList<>();
         int i = 0;
-        for (MobileLedgerProfile p : list) {
-            if (!p.isPostingPermitted()) continue;
-
-            ShortcutInfo si = new ShortcutInfo.Builder(this, "new_transaction_" + p.getUuid())
-                    .setShortLabel(p.getName())
-                    .setIcon(Icon.createWithResource(this, R.drawable.svg_thick_plus_white))
-                    .setIntent(
-                            new Intent(Intent.ACTION_VIEW, null, this, NewTransactionActivity.class)
-                                    .putExtra("profile_uuid", p.getUuid())).setRank(i).build();
+        for (Profile p : list) {
+            if (shortcuts.size() >= sm.getMaxShortcutCountPerActivity())
+                break;
+
+            if (!p.permitPosting())
+                continue;
+
+            final ShortcutInfo.Builder builder =
+                    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(
+                                             ProfileThemedActivity.PARAM_PROFILE_ID, p.getId())
+                                                                          .putExtra(
+                                                                                  ProfileThemedActivity.PARAM_THEME,
+                                                                                  p.getTheme()))
+                                     .setRank(i)
+                                     .build();
             shortcuts.add(si);
             i++;
         }
             shortcuts.add(si);
             i++;
         }
-        ShortcutManager sm = getSystemService(ShortcutManager.class);
         sm.setDynamicShortcuts(shortcuts);
     }
         sm.setDynamicShortcuts(shortcuts);
     }
-    private void onProfileListChanged(List<MobileLedgerProfile> newList) {
-        if (newList == null) {
-            // profiles not yet loaded from DB
-            findViewById(R.id.loading_layout).setVisibility(View.VISIBLE);
-            findViewById(R.id.no_profiles_layout).setVisibility(View.GONE);
-            findViewById(R.id.pager_layout).setVisibility(View.GONE);
-            return;
-        }
+    private void onProfileListChanged(@NotNull List<Profile> newList) {
+        createShortcuts(newList);
 
         if (newList.isEmpty()) {
 
         if (newList.isEmpty()) {
-            findViewById(R.id.no_profiles_layout).setVisibility(View.VISIBLE);
-            findViewById(R.id.pager_layout).setVisibility(View.GONE);
-            findViewById(R.id.loading_layout).setVisibility(View.GONE);
+            b.noProfilesLayout.setVisibility(View.VISIBLE);
+            b.mainAppLayout.setVisibility(View.GONE);
             return;
         }
 
             return;
         }
 
-        findViewById(R.id.pager_layout).setVisibility(View.VISIBLE);
-        findViewById(R.id.no_profiles_layout).setVisibility(View.GONE);
-        findViewById(R.id.loading_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()));
 
                 (int) (getResources().getDimension(R.dimen.thumb_row_height) * newList.size()));
 
-        debug("profiles", "profile list changed");
-        mProfileListAdapter.notifyDataSetChanged();
+        Logger.debug("profiles", "profile list changed");
+        mProfileListAdapter.setProfileList(newList);
 
 
-        createShortcuts(newList);
+        final Profile currentProfile = Data.getProfile();
+        Profile replacementProfile = null;
+        if (currentProfile != null) {
+            for (Profile p : newList) {
+                if (p.getId() == currentProfile.getId()) {
+                    replacementProfile = p;
+                    break;
+                }
+            }
+        }
+
+        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
      */
     }
     /**
      * called when the current profile has changed
      */
-    private void onProfileChanged(MobileLedgerProfile profile) {
-        boolean haveProfile = profile != null;
-        findViewById(R.id.no_profiles_layout).setVisibility(haveProfile ? View.GONE : View.VISIBLE);
-        findViewById(R.id.pager_layout).setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
-
-        if (haveProfile) setTitle(profile.getName());
-        else setTitle(R.string.app_name);
+    private void onProfileChanged(@Nullable Profile newProfile) {
+        if (this.profile != null) {
+            if (this.profile.equals(newProfile))
+                return;
+        }
 
 
-        this.profile = profile;
+        boolean haveProfile = newProfile != null;
 
 
-        mProfileListAdapter.notifyDataSetChanged();
+        if (haveProfile)
+            setTitle(newProfile.getName());
+        else
+            setTitle(R.string.app_name);
 
 
-        int newProfileTheme = haveProfile ? profile.getThemeId() : -1;
+        int newProfileTheme = haveProfile ? newProfile.getTheme() : Colors.DEFAULT_HUE_DEG;
         if (newProfileTheme != Colors.profileThemeId) {
         if (newProfileTheme != Colors.profileThemeId) {
-            debug("profiles",
+            Logger.debug("profiles",
                     String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId,
                             newProfileTheme));
                     String.format(Locale.ENGLISH, "profile theme %d → %d", Colors.profileThemeId,
                             newProfileTheme));
-            MainActivity.this.profileThemeChanged();
             Colors.profileThemeId = newProfileTheme;
             Colors.profileThemeId = newProfileTheme;
+            profileThemeChanged();
             // profileThemeChanged would restart the activity, so no need to reload the
             // data sets below
             return;
         }
 
             // profileThemeChanged would restart the activity, so no need to reload the
             // data sets below
             return;
         }
 
-        drawer.closeDrawers();
+        final boolean sameProfileId = (newProfile != null) && (this.profile != null) &&
+                                      this.profile.getId() == newProfile.getId();
 
 
-        Data.transactions.clear();
-        debug("transactions", "requesting list reload");
-        TransactionListViewModel.scheduleTransactionListReload();
+        this.profile = newProfile;
 
 
-        Data.accounts.clear();
-        AccountSummaryViewModel.scheduleAccountListReload();
+        b.noProfilesLayout.setVisibility(haveProfile ? View.GONE : View.VISIBLE);
+        b.pagerLayout.setVisibility(haveProfile ? View.VISIBLE : View.VISIBLE);
+
+        mProfileListAdapter.notifyDataSetChanged();
 
         if (haveProfile) {
 
         if (haveProfile) {
-            if (profile.isPostingPermitted()) {
-                mToolbar.setSubtitle(null);
-                fab.show();
+            if (newProfile.permitPosting()) {
+                b.toolbar.setSubtitle(null);
+                b.btnAddTransaction.show();
             }
             else {
             }
             else {
-                mToolbar.setSubtitle(R.string.profile_subitlte_read_only);
-                fab.hide();
+                b.toolbar.setSubtitle(R.string.profile_subtitle_read_only);
+                b.btnAddTransaction.hide();
             }
         }
         else {
             }
         }
         else {
-            mToolbar.setSubtitle(null);
-            fab.hide();
+            b.toolbar.setSubtitle(null);
+            b.btnAddTransaction.hide();
         }
 
         updateLastUpdateTextFromDB();
         }
 
         updateLastUpdateTextFromDB();
-    }
-    private void updateLastUpdateDisplay(Date newValue) {
-        LinearLayout l = findViewById(R.id.transactions_last_update_layout);
-        TextView v = findViewById(R.id.transactions_last_update);
-        if (newValue == null) {
-            l.setVisibility(View.INVISIBLE);
-            debug("main", "no last update date :(");
-        }
-        else {
-            final String text = DateFormat.getDateTimeInstance().format(newValue);
-            v.setText(text);
-            l.setVisibility(View.VISIBLE);
-            debug("main", String.format("Date formatted: %s", text));
+
+        if (sameProfileId) {
+            Logger.debug(TAG, String.format(Locale.ROOT, "Short-cut profile 'changed' to %d",
+                    newProfile.getId()));
+            return;
         }
 
         }
 
-        scheduleDataRetrievalIfStale(newValue);
-    }
-    private void profileThemeChanged() {
-        Bundle bundle = new Bundle();
-        onSaveInstanceState(bundle);
+        mainModel.getAccountFilter()
+                 .observe(this, this::onAccountFilterChanged);
 
 
-        storeThemeIdInPrefs(profile.getThemeId());
+        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);
+            }
+        }
 
 
-        // restart activity to reflect theme change
-        finish();
+        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
         // un-hook all observed LiveData
-        Data.profile.removeObservers(this);
+        Data.removeProfileObservers(this);
         Data.profiles.removeObservers(this);
         Data.profiles.removeObservers(this);
+        Data.lastUpdateTransactionCount.removeObservers(this);
+        Data.lastUpdateAccountCount.removeObservers(this);
         Data.lastUpdateDate.removeObservers(this);
         Data.lastUpdateDate.removeObservers(this);
-        Intent intent = new Intent(this, this.getClass());
-        intent.putExtra(BUNDLE_SAVED_STATE, bundle);
-        startActivity(intent);
-    }
-    private void storeThemeIdInPrefs(int themeId) {
-        // store the new theme id in the preferences
-        SharedPreferences prefs = getPreferences(MODE_PRIVATE);
-        SharedPreferences.Editor e = prefs.edit();
-        e.putInt(PREF_THEME_ID, themeId);
-        e.apply();
-    }
-    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);
-    }
-    private void setupProfile() {
-        MLDB.getOption(MLDB.OPT_PROFILE_UUID, null, new GetOptCallback() {
-            @Override
-            protected void onResult(String profileUUID) {
-                MobileLedgerProfile startupProfile;
 
 
-                startupProfile = Data.getProfile(profileUUID);
-                Data.setCurrentProfile(startupProfile);
-            }
-        });
+        Logger.debug(TAG, "profileThemeChanged(): recreating activity");
+        recreate();
     }
     public void fabNewTransactionClicked(View view) {
         Intent intent = new Intent(this, NewTransactionActivity.class);
     }
     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);
     }
         startActivity(intent);
         overridePendingTransition(R.anim.slide_in_up, R.anim.dummy);
     }
-    public void navSettingsClicked(View view) {
-        Intent intent = new Intent(this, SettingsActivity.class);
-        startActivity(intent);
-        drawer.closeDrawers();
-    }
     public void markDrawerItemCurrent(int id) {
     public void markDrawerItemCurrent(int id) {
-        TextView item = drawer.findViewById(id);
+        TextView item = b.drawerLayout.findViewById(id);
         item.setBackgroundColor(Colors.tableRowDarkBG);
 
         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) {
             if (view.getId() != id) {
                 view.setBackgroundColor(Color.TRANSPARENT);
             }
         }
     }
     public void onAccountSummaryClicked(View view) {
-        drawer.closeDrawers();
+        b.drawerLayout.closeDrawers();
 
         showAccountSummaryFragment();
     }
     private void showAccountSummaryFragment() {
 
         showAccountSummaryFragment();
     }
     private void showAccountSummaryFragment() {
-        mViewPager.setCurrentItem(0, true);
-        Data.accountFilter.setValue(null);
+        b.mainPager.setCurrentItem(0, true);
+        mainModel.getAccountFilter()
+                 .setValue(null);
     }
     public void onLatestTransactionsClicked(View view) {
     }
     public void onLatestTransactionsClicked(View view) {
-        drawer.closeDrawers();
+        b.drawerLayout.closeDrawers();
 
 
-        showTransactionsFragment((String) null);
-    }
-    private void showTransactionsFragment(String accName) {
-        Data.accountFilter.setValue(accName);
-        mViewPager.setCurrentItem(1, true);
+        showTransactionsFragment(null);
     }
     }
-    private void showTransactionsFragment(LedgerAccount account) {
-        showTransactionsFragment((account == null) ? null : account.getName());
+    public void showTransactionsFragment(String accName) {
+        mainModel.getAccountFilter()
+                 .setValue(accName);
+        b.mainPager.setCurrentItem(1, true);
     }
     }
-    public void showAccountTransactions(LedgerAccount account) {
+    public void showAccountTransactions(String accountName) {
         mBackMeansToAccountList = true;
         mBackMeansToAccountList = true;
-        showTransactionsFragment(account);
+        showTransactionsFragment(accountName);
     }
     @Override
     public void onBackPressed() {
     }
     @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 {
         }
         else {
-            if (mBackMeansToAccountList && (mViewPager.getCurrentItem() == 1)) {
-                Data.accountFilter.setValue(null);
+            if (mBackMeansToAccountList && (b.mainPager.getCurrentItem() == 1)) {
+                mainModel.getAccountFilter()
+                         .setValue(null);
                 showAccountSummaryFragment();
                 mBackMeansToAccountList = false;
             }
             else {
                 showAccountSummaryFragment();
                 mBackMeansToAccountList = false;
             }
             else {
-                debug("fragments", String.format(Locale.ENGLISH, "manager stack: %d",
+                Logger.debug(TAG, String.format(Locale.ENGLISH, "manager stack: %d",
                         getSupportFragmentManager().getBackStackEntryCount()));
 
                 super.onBackPressed();
                         getSupportFragmentManager().getBackStackEntryCount()));
 
                 super.onBackPressed();
@@ -538,179 +584,172 @@ public class MainActivity extends ProfileThemedActivity {
         }
     }
     public void updateLastUpdateTextFromDB() {
         }
     }
     public void updateLastUpdateTextFromDB() {
-        if (profile == null) return;
-
-        long last_update = profile.getLongOption(MLDB.OPT_LAST_SCRAPE, 0L);
+        if (profile == null)
+            return;
 
 
-        debug("transactions", String.format(Locale.ENGLISH, "Last update = %d", last_update));
-        if (last_update == 0) {
-            Data.lastUpdateDate.postValue(null);
+        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 |
+                                DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NUMERIC_DATE;
+        String templateForTransactions =
+                getResources().getString(R.string.transaction_count_summary);
+        String templateForAccounts = getResources().getString(R.string.account_count_summary);
+        Integer accountCount = Data.lastUpdateAccountCount.getValue();
+        Integer transactionCount = Data.lastUpdateTransactionCount.getValue();
+        Date lastUpdate = Data.lastUpdateDate.getValue();
+        if (lastUpdate == null) {
+            Data.lastTransactionsUpdateText.setValue("----");
+            Data.lastAccountsUpdateText.setValue("----");
         }
         else {
         }
         else {
-            Data.lastUpdateDate.postValue(new Date(last_update));
+            Data.lastTransactionsUpdateText.setValue(
+                    String.format(Objects.requireNonNull(Data.locale.getValue()),
+                            templateForTransactions,
+                            transactionCount == null ? 0 : transactionCount,
+                            DateUtils.formatDateTime(this, lastUpdate.getTime(), formatFlags)));
+            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) {
         }
     }
     public void onStopTransactionRefreshClick(View view) {
-        debug("interactive", "Cancelling transactions refresh");
-        Data.stopTransactionsRetrieval();
-        bTransactionListCancelDownload.setEnabled(false);
-    }
-    public void onRetrieveDone(String error) {
-        Data.transactionRetrievalDone();
-        findViewById(R.id.transaction_progress_layout).setVisibility(View.GONE);
-
-        if (error == null) {
-            updateLastUpdateTextFromDB();
-
-            new RefreshDescriptionsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-            TransactionListViewModel.scheduleTransactionListReload();
-        }
-        else Toast.makeText(this, error, Toast.LENGTH_LONG).show();
-    }
-    public void onRetrieveStart() {
-        ProgressBar progressBar = findViewById(R.id.transaction_list_progress_bar);
-        bTransactionListCancelDownload.setEnabled(true);
-        ColorStateList csl = Colors.getColorStateList();
-        progressBar.setIndeterminateTintList(csl);
-        progressBar.setProgressTintList(csl);
-        progressBar.setIndeterminate(true);
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) progressBar.setProgress(0, false);
-        else progressBar.setProgress(0);
-        findViewById(R.id.transaction_progress_layout).setVisibility(View.VISIBLE);
-    }
-    public void onRetrieveProgress(RetrieveTransactionsTask.Progress progress) {
-        ProgressBar progressBar = findViewById(R.id.transaction_list_progress_bar);
-        if ((progress.getTotal() == RetrieveTransactionsTask.Progress.INDETERMINATE) ||
-            (progress.getTotal() == 0))
-        {
-            progressBar.setIndeterminate(true);
+        Logger.debug(TAG, "Cancelling transactions refresh");
+        mainModel.stopTransactionsRetrieval();
+        b.transactionListCancelDownload.setEnabled(false);
+    }
+    public void onRetrieveRunningChanged(Boolean running) {
+        if (running) {
+            b.transactionListCancelDownload.setEnabled(true);
+            ColorStateList csl = Colors.getColorStateList();
+            b.transactionListProgressBar.setIndeterminateTintList(csl);
+            b.transactionListProgressBar.setProgressTintList(csl);
+            b.transactionListProgressBar.setIndeterminate(true);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                b.transactionListProgressBar.setProgress(0, false);
+            }
+            else {
+                b.transactionListProgressBar.setProgress(0);
+            }
+            b.transactionProgressLayout.setVisibility(View.VISIBLE);
         }
         else {
         }
         else {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                progressBar.setMin(0);
+            b.transactionProgressLayout.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();
+
+            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;
             }
             }
-            progressBar.setMax(progress.getTotal());
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                progressBar.setProgress(progress.getProgress(), true);
+
+            return;
+        }
+
+
+        b.transactionListCancelDownload.setEnabled(true);
+//        ColorStateList csl = Colors.getColorStateList();
+//        progressBar.setIndeterminateTintList(csl);
+//        progressBar.setProgressTintList(csl);
+//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+//            progressBar.setProgress(0, false);
+//        else
+//            progressBar.setProgress(0);
+        b.transactionProgressLayout.setVisibility(View.VISIBLE);
+
+        if (progress.isIndeterminate() || (progress.getTotal() <= 0)) {
+            b.transactionListProgressBar.setIndeterminate(true);
+            Logger.debug(TAG, "progress: indeterminate");
+        }
+        else {
+            if (b.transactionListProgressBar.isIndeterminate()) {
+                b.transactionListProgressBar.setIndeterminate(false);
             }
             }
-            else progressBar.setProgress(progress.getProgress());
-            progressBar.setIndeterminate(false);
+//            Logger.debug(TAG,
+//                    String.format(Locale.US, "progress: %d/%d", progress.getProgress(),
+//                    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)
+                b.transactionListProgressBar.setProgress(progress.getProgress(), false);
+            else
+                b.transactionListProgressBar.setProgress(progress.getProgress());
         }
     }
     public void fabShouldShow() {
         }
     }
     public void fabShouldShow() {
-        if ((profile != null) && profile.isPostingPermitted()) fab.show();
-    }
-    public void fabHide() {
-        fab.hide();
-    }
-    public void onAccountSummaryRowViewClicked(View view) {
-        ViewGroup row;
-        if (view.getId() == R.id.account_expander) row = (ViewGroup) view.getParent().getParent();
-        else row = (ViewGroup) view.getParent();
-
-        LedgerAccount acc = (LedgerAccount) row.getTag();
-        switch (view.getId()) {
-            case R.id.account_row_acc_name:
-            case R.id.account_expander:
-            case R.id.account_expander_container:
-                debug("accounts", "Account expander clicked");
-                if (!acc.hasSubAccounts()) return;
-
-                boolean wasExpanded = acc.isExpanded();
-
-                View arrow = row.findViewById(R.id.account_expander_container);
-
-                arrow.clearAnimation();
-                ViewPropertyAnimator animator = arrow.animate();
-
-                acc.toggleExpanded();
-                DbOpQueue.add("update accounts set expanded=? where name=? and profile=?",
-                        new Object[]{acc.isExpanded(), acc.getName(), profile.getUuid()
-                        });
-
-                if (wasExpanded) {
-                    debug("accounts", String.format("Collapsing account '%s'", acc.getName()));
-                    arrow.setRotation(0);
-                    animator.rotationBy(180);
-
-                    // removing all child accounts from the view
-                    int start = -1, count = 0;
-                    try (LockHolder ignored = Data.accounts.lockForWriting()) {
-                        for (int i = 0; i < Data.accounts.size(); i++) {
-                            if (acc.isParentOf(Data.accounts.get(i))) {
-//                                debug("accounts", String.format("Found a child '%s' at position %d",
-//                                        Data.accounts.get(i).getName(), i));
-                                if (start == -1) {
-                                    start = i;
-                                }
-                                count++;
-                            }
-                            else {
-                                if (start != -1) {
-//                                    debug("accounts",
-//                                            String.format("Found a non-child '%s' at position %d",
-//                                                    Data.accounts.get(i).getName(), i));
-                                    break;
-                                }
-                            }
-                        }
-
-                        if (start != -1) {
-                            for (int j = 0; j < count; j++) {
-//                                debug("accounts", String.format("Removing item %d: %s", start + j,
-//                                        Data.accounts.get(start).getName()));
-                                Data.accounts.removeQuietly(start);
-                            }
-
-                            mAccountSummaryFragment.modelAdapter
-                                    .notifyItemRangeRemoved(start, count);
-                        }
-                    }
-                }
-                else {
-                    debug("accounts", String.format("Expanding account '%s'", acc.getName()));
-                    arrow.setRotation(180);
-                    animator.rotationBy(-180);
-                    List<LedgerAccount> children = profile.loadVisibleChildAccountsOf(acc);
-                    try (LockHolder ignored = Data.accounts.lockForWriting()) {
-                        int parentPos = Data.accounts.indexOf(acc);
-                        if (parentPos != -1) {
-                            // may have disappeared in a concurrent refresh operation
-                            Data.accounts.addAllQuietly(parentPos + 1, children);
-                            mAccountSummaryFragment.modelAdapter
-                                    .notifyItemRangeInserted(parentPos + 1, children.size());
-                        }
-                    }
-                }
-                break;
-            case R.id.account_row_acc_amounts:
-                if (acc.getAmountCount() > AccountSummaryAdapter.AMOUNT_LIMIT) {
-                    acc.toggleAmountsExpanded();
-                    DbOpQueue
-                            .add("update accounts set amounts_expanded=? where name=? and profile=?",
-                                    new Object[]{acc.amountsExpanded(), acc.getName(),
-                                                 profile.getUuid()
-                                    });
-                    Data.accounts.triggerItemChangedNotification(acc);
-                }
-                break;
-        }
+        if ((profile != null) && profile.permitPosting() && !b.drawerLayout.isOpen())
+            fabManager.showFab();
     }
     }
+    @Override
+    public Context getContext() {
+        return this;
+    }
+    @Override
+    public void showManagedFab() {
+        fabShouldShow();
+    }
+    @Override
+    public void hideManagedFab() {
+        fabManager.hideFab();
+    }
+    public static class SectionsPagerAdapter extends FragmentStateAdapter {
 
 
-    public class SectionsPagerAdapter extends FragmentPagerAdapter {
-
-        SectionsPagerAdapter(FragmentManager fm) {
-            super(fm);
+        public SectionsPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
+            super(fragmentActivity);
         }
         }
-
         @NotNull
         @Override
         @NotNull
         @Override
-        public Fragment getItem(int position) {
-            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:
             switch (position) {
                 case 0:
-//                    debug("flow", "Creating account summary fragment");
-                    return mAccountSummaryFragment = new AccountSummaryFragment();
+//                    debug(TAG, "Creating account summary fragment");
+                    return new AccountSummaryFragment();
                 case 1:
                     return new TransactionListFragment();
                 default:
                 case 1:
                     return new TransactionListFragment();
                 default:
@@ -720,8 +759,41 @@ public class MainActivity extends ProfileThemedActivity {
         }
 
         @Override
         }
 
         @Override
-        public int getCount() {
+        public int getItemCount() {
             return 2;
         }
     }
             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));
+        }
+    }
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionActivity.java
deleted file mode 100644 (file)
index d506ac3..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * 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.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-
-import androidx.appcompat.widget.Toolbar;
-import androidx.lifecycle.ViewModelProviders;
-import androidx.navigation.NavController;
-import androidx.navigation.Navigation;
-
-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;
-
-/*
- * TODO: nicer progress while transaction is submitted
- * TODO: reports
- * TODO: get rid of the custom session/cookie and auth code?
- *         (the last problem with the POST was the missing content-length header)
- *  */
-
-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.profile.observe(this,
-                mobileLedgerProfile -> toolbar.setSubtitle(mobileLedgerProfile.getName()));
-
-        navController = Navigation.findNavController(this, R.id.new_transaction_nav);
-
-        Objects.requireNonNull(getSupportActionBar())
-               .setDisplayHomeAsUpEnabled(true);
-
-        model = ViewModelProviders.of(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);
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        // FIXME if (tvDescription.getText().toString().isEmpty()) tvDescription.requestFocus();
-    }
-    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();
-    }
-
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionFragment.java
deleted file mode 100644 (file)
index 10f246d..0000000
+++ /dev/null
@@ -1,263 +0,0 @@
-/*
- * 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.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.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.ViewModelProviders;
-import androidx.recyclerview.widget.ItemTouchHelper;
-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 org.jetbrains.annotations.NotNull;
-
-import java.util.Date;
-
-/**
- * A simple {@link Fragment} subclass.
- * Activities that contain this fragment must implement the
- * {@link OnNewTransactionFragmentInteractionListener} interface
- * to handle interaction events.
- */
-public class NewTransactionFragment extends Fragment {
-    private NewTransactionItemsAdapter listAdapter;
-    private NewTransactionModel viewModel;
-    private RecyclerView list;
-    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);
-        inflater.inflate(R.menu.new_transaction_fragment, menu);
-        menu.findItem(R.id.action_reset_new_transaction_activity)
-            .setOnMenuItemClickListener(item -> {
-                listAdapter.reset();
-                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 onActivityCreated(@Nullable Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-        FragmentActivity activity = getActivity();
-        if (activity == null)
-            throw new RSInvalidStateException(
-                    "getActivity() returned null within onActivityCreated()");
-
-        list = activity.findViewById(R.id.new_transaction_accounts);
-        viewModel = ViewModelProviders.of(activity)
-                                      .get(NewTransactionModel.class);
-        mProfile = Data.profile.getValue();
-        listAdapter = new NewTransactionItemsAdapter(viewModel, mProfile);
-        list.setAdapter(listAdapter);
-        list.setLayoutManager(new LinearLayoutManager(activity));
-        Data.profile.observe(this, profile -> {
-            mProfile = profile;
-            listAdapter.setProfile(profile);
-        });
-        listAdapter.notifyDataSetChanged();
-        new ItemTouchHelper(new ItemTouchHelper.Callback() {
-            @Override
-            public int getMovementFlags(@NonNull RecyclerView recyclerView,
-                                        @NonNull RecyclerView.ViewHolder viewHolder) {
-                int flags = makeFlag(ItemTouchHelper.ACTION_STATE_IDLE, ItemTouchHelper.END);
-                // the top item is always there (date and description)
-                if (viewHolder.getAdapterPosition() > 0) {
-                    if (viewModel.getAccountCount() > 2) {
-                        flags |= 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) {
-                return false;
-            }
-            @Override
-            public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
-                if (viewModel.getAccountCount() == 2)
-                    Snackbar.make(list, R.string.msg_at_least_two_accounts_are_required,
-                            Snackbar.LENGTH_LONG)
-                            .setAction("Action", null)
-                            .show();
-                else {
-                    int pos = viewHolder.getAdapterPosition();
-                    viewModel.removeItem(pos - 1);
-                    listAdapter.notifyItemRemoved(pos);
-                    viewModel.sendCountNotifications(); // needed after items re-arrangement
-                    viewModel.checkTransactionSubmittable(listAdapter);
-                }
-            }
-        }).attachToRecyclerView(list);
-
-        viewModel.isSubmittable()
-                 .observe(this, isSubmittable -> {
-                     if (isSubmittable) {
-                         if (fab != null) {
-                             fab.show();
-                             fab.setEnabled(true);
-                         }
-                     }
-                     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) {
-                // TODO display error
-                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);
-        }
-    }
-    @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.setEnabled(false);
-        Misc.hideSoftKeyboard(this);
-        if (mListener != null) {
-            Date date = viewModel.getDate();
-            LedgerTransaction tr =
-                    new LedgerTransaction(null, date, viewModel.getDescription(), mProfile);
-
-            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);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemHolder.java
deleted file mode 100644 (file)
index 9ee32ea..0000000
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * 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.annotation.SuppressLint;
-import android.os.Build;
-import android.text.Editable;
-import android.text.TextWatcher;
-import android.text.method.DigitsKeyListener;
-import android.view.View;
-import android.view.inputmethod.EditorInfo;
-import android.widget.AutoCompleteTextView;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-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.Data;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.AutoCompleteTextViewWithClear;
-import net.ktnx.mobileledger.ui.DatePickerFragment;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.MLDB;
-import net.ktnx.mobileledger.utils.Misc;
-
-import java.text.DecimalFormatSymbols;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-
-class NewTransactionItemHolder extends RecyclerView.ViewHolder
-        implements DatePickerFragment.DatePickedListener, DescriptionSelectedCallback {
-    private final String decimalSeparator;
-    private final String decimalDot;
-    private NewTransactionModel.Item item;
-    private TextView tvDate;
-    private AutoCompleteTextView tvDescription;
-    private AutoCompleteTextView tvAccount;
-    private TextView tvAmount;
-    private LinearLayout lHead;
-    private LinearLayout lAccount;
-    private FrameLayout lPadding;
-    private MobileLedgerProfile mProfile;
-    private Date date;
-    private Observer<Date> dateObserver;
-    private Observer<String> descriptionObserver;
-    private Observer<String> hintObserver;
-    private Observer<Integer> focusedAccountObserver;
-    private Observer<Integer> accountCountObserver;
-    private Observer<Boolean> editableObserver;
-    private boolean inUpdate = false;
-    private boolean syncingData = false;
-    NewTransactionItemHolder(@NonNull View itemView, NewTransactionItemsAdapter adapter) {
-        super(itemView);
-        tvAccount = itemView.findViewById(R.id.account_row_acc_name);
-        tvAmount = itemView.findViewById(R.id.account_row_acc_amounts);
-        tvDate = itemView.findViewById(R.id.new_transaction_date);
-        tvDescription = itemView.findViewById(R.id.new_transaction_description);
-        lHead = itemView.findViewById(R.id.ntr_data);
-        lAccount = itemView.findViewById(R.id.ntr_account);
-        lPadding = itemView.findViewById(R.id.ntr_padding);
-
-        tvDescription.setNextFocusForwardId(View.NO_ID);
-        tvAccount.setNextFocusForwardId(View.NO_ID);
-        tvAmount.setNextFocusForwardId(View.NO_ID); // magic!
-
-        tvDate.setOnClickListener(v -> pickTransactionDate());
-
-        mProfile = Data.profile.getValue();
-        if (mProfile == null)
-            throw new AssertionError();
-
-        View.OnFocusChangeListener focusMonitor = (v, hasFocus) -> {
-            if (hasFocus) {
-                boolean wasSyncing = syncingData;
-                syncingData = true;
-                try {
-                    final int pos = getAdapterPosition();
-                    adapter.updateFocusedItem(pos);
-                    if (v instanceof AutoCompleteTextViewWithClear) {
-                        adapter.noteFocusIsOnAccount(pos);
-                    }
-                    else {
-                        adapter.noteFocusIsOnAmount(pos);
-                    }
-                }
-                finally {
-                    syncingData = wasSyncing;
-                }
-            }
-        };
-
-        tvDescription.setOnFocusChangeListener(focusMonitor);
-        tvAccount.setOnFocusChangeListener(focusMonitor);
-        tvAmount.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);
-
-        // FIXME: react on configuration (locale) changes
-        decimalSeparator = String.valueOf(DecimalFormatSymbols.getInstance()
-                                                              .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.model.checkTransactionSubmittable(adapter);
-                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) {
-                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
-                    // only one decimal separator is allowed
-                    // plus and minus are allowed only at the beginning
-                    String val = s.toString();
-                    if (val.isEmpty())
-                        tvAmount.setKeyListener(DigitsKeyListener.getInstance(
-                                "0123456789+-" + decimalSeparator + decimalDot));
-                    else if (val.contains(decimalSeparator) || val.contains(decimalDot))
-                        tvAmount.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
-                    else
-                        tvAmount.setKeyListener(DigitsKeyListener.getInstance(
-                                "0123456789" + decimalSeparator + decimalDot));
-
-                    syncData();
-                    adapter.model.checkTransactionSubmittable(adapter);
-                }
-            }
-        };
-        tvDescription.addTextChangedListener(tw);
-        tvAccount.addTextChangedListener(tw);
-        tvAmount.addTextChangedListener(amountWatcher);
-
-        // FIXME: react on locale changes
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
-            tvAmount.setKeyListener(DigitsKeyListener.getInstance(Locale.getDefault(), true, true));
-        else
-            tvAmount.setKeyListener(
-                    DigitsKeyListener.getInstance("0123456789+-" + decimalSeparator + decimalDot));
-
-        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;
-            }
-        };
-        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;
-        focusedAccountObserver = index -> {
-            if ((index != null) && index.equals(getAdapterPosition())) {
-                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();
-                        boolean focused = tvDescription.requestFocus();
-                        tvDescription.dismissDropDown();
-                        if (focused)
-                            Misc.showSoftKeyboard(
-                                    (NewTransactionActivity) tvDescription.getContext());
-                        break;
-                    case transactionRow:
-                        // do nothing if a row element already has the focus
-                        if (!itemView.hasFocus()) {
-                            if (item.focusIsOnAmount()) {
-                                tvAmount.requestFocus();
-                            }
-                            else {
-                                focused = tvAccount.requestFocus();
-                                tvAccount.dismissDropDown();
-                                if (focused)
-                                    Misc.showSoftKeyboard(
-                                            (NewTransactionActivity) tvAccount.getContext());
-                            }
-                        }
-
-                        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() ==
-                                                                         NewTransactionModel.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);
-        };
-    }
-    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
-     */
-    private void syncData() {
-        if (item == null)
-            return;
-
-        if (syncingData) {
-            Logger.debug("new-trans", "skipping syncData() loop");
-            return;
-        }
-
-        syncingData = true;
-
-        try {
-            switch (item.getType()) {
-                case generalData:
-                    item.setDate(String.valueOf(tvDate.getText()));
-                    item.setDescription(String.valueOf(tvDescription.getText()));
-                    break;
-                case transactionRow:
-                    item.getAccount()
-                        .setAccountName(String.valueOf(tvAccount.getText()));
-
-                    // TODO: handle multiple amounts
-                    String amount = String.valueOf(tvAmount.getText());
-                    amount = amount.trim();
-
-                    if (amount.isEmpty()) {
-                        item.getAccount()
-                            .resetAmount();
-                    }
-                    else {
-                        try {
-                            amount = amount.replace(decimalSeparator, decimalDot);
-                            item.getAccount()
-                                .setAmount(Float.parseFloat(amount));
-                        }
-                        catch (NumberFormatException e) {
-                            Logger.debug("new-trans", String.format(
-                                    "assuming amount is not set due to number format exception. " +
-                                    "input was '%s'", amount));
-                            item.getAccount()
-                                .resetAmount();
-                        }
-                    }
-
-                    break;
-                case bottomFiller:
-                    throw new RuntimeException("Should not happen");
-            }
-        }
-        finally {
-            syncingData = false;
-        }
-    }
-    private void pickTransactionDate() {
-        DatePickerFragment picker = new DatePickerFragment();
-        picker.setFutureDates(mProfile.getFutureDates());
-        picker.setOnDatePickedListener(this);
-        picker.show(((NewTransactionActivity) tvDate.getContext()).getSupportFragmentManager(),
-                "datePicker");
-    }
-    /**
-     * 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.stopObservingAmountHint(hintObserver);
-                this.item.stopObservingEditableFlag(editableObserver);
-                this.item.getModel()
-                         .stopObservingFocusedItem(focusedAccountObserver);
-                this.item.getModel()
-                         .stopObservingAccountCount(accountCountObserver);
-
-                this.item = null;
-            }
-
-            switch (item.getType()) {
-                case generalData:
-                    tvDate.setText(item.getFormattedDate());
-                    tvDescription.setText(item.getDescription());
-                    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());
-                    if (acc.isAmountSet()) {
-                        tvAmount.setText(String.format("%1.2f", acc.getAmount()));
-                    }
-                    else {
-                        tvAmount.setText("");
-//                        tvAmount.setHint(R.string.zero_amount);
-                    }
-                    tvAmount.setHint(item.getAmountHint());
-                    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();
-                item.observeDate(activity, dateObserver);
-                item.observeDescription(activity, descriptionObserver);
-                item.observeAmountHint(activity, hintObserver);
-                item.observeEditableFlag(activity, editableObserver);
-                item.getModel()
-                    .observeFocusedItem(activity, focusedAccountObserver);
-                item.getModel()
-                    .observeAccountCount(activity, accountCountObserver);
-            }
-        }
-        finally {
-            endUpdates();
-        }
-    }
-    @Override
-    public void onDatePicked(int year, int month, int day) {
-        final Calendar c = GregorianCalendar.getInstance();
-        c.set(year, month, day);
-        item.setDate(c.getTime());
-        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);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionItemsAdapter.java
deleted file mode 100644 (file)
index cb03f3d..0000000
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * 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.database.Cursor;
-import android.view.LayoutInflater;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import net.ktnx.mobileledger.App;
-import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.async.DescriptionSelectedCallback;
-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 java.util.ArrayList;
-import java.util.Locale;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-class NewTransactionItemsAdapter extends RecyclerView.Adapter<NewTransactionItemHolder>
-        implements DescriptionSelectedCallback {
-    NewTransactionModel model;
-    private MobileLedgerProfile mProfile;
-    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();
-        }
-    }
-    public void setProfile(MobileLedgerProfile profile) {
-        mProfile = profile;
-    }
-    int addRow() {
-        final int newAccountCount = model.addAccount(new LedgerTransactionAccount(""));
-        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;
-    }
-    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;
-    }
-    public void descriptionSelected(String description) {
-        debug("descr 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 where t.description=?");
-        params.add(description);
-
-        if (accFilter != null) {
-            sb.append(" AND EXISTS (")
-              .append("SELECT 1 FROM transaction_accounts ta ")
-              .append("WHERE ta.profile = t.profile")
-              .append(" AND ta.transaction_id = t.id")
-              .append(" AND UPPER(ta.account_name) LIKE '%'||?||'%')");
-            params.add(accFilter.toUpperCase());
-        }
-
-        sb.append(" ORDER BY date desc limit 1");
-
-        final String sql = sb.toString();
-        debug("descr", sql);
-        debug("descr", params.toString());
-
-        try (Cursor c = App.getDatabase()
-                           .rawQuery(sql, params.toArray(new String[]{})))
-        {
-            if (!c.moveToNext())
-                return;
-
-            String profileUUID = c.getString(0);
-            int transactionId = c.getInt(1);
-            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 with description %s", profileUUID, transactionId,
-                        description));
-
-            tr = profile.loadTransaction(transactionId);
-            ArrayList<LedgerTransactionAccount> accounts = tr.getAccounts();
-            NewTransactionModel.Item firstNegative = null;
-            boolean singleNegative = false;
-            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());
-                if (acc.isAmountSet()) {
-                    item.getAccount()
-                        .setAmount(acc.getAmount());
-                    if (acc.getAmount() < 0) {
-                        if (firstNegative == null) {
-                            firstNegative = item;
-                            singleNegative = true;
-                        }
-                        else
-                            singleNegative = false;
-                    }
-                }
-                else
-                    item.getAccount()
-                        .resetAmount();
-                notifyItemChanged(i + 1);
-            }
-
-            if (singleNegative) {
-                firstNegative.getAccount()
-                             .resetAmount();
-            }
-        }
-        model.checkTransactionSubmittable(this);
-        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)?
-        }
-    }
-    public 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
-    }
-    public void updateFocusedItem(int position) {
-        model.updateFocusedItem(position);
-    }
-    public void noteFocusIsOnAccount(int position) {
-        model.noteFocusIsOnAccount(position);
-    }
-    public void noteFocusIsOnAmount(int position) {
-        model.noteFocusIsOnAmount(position);
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/NewTransactionModel.java
deleted file mode 100644 (file)
index 1f4cec5..0000000
+++ /dev/null
@@ -1,506 +0,0 @@
-/*
- * 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.annotation.SuppressLint;
-
-import androidx.annotation.NonNull;
-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.model.LedgerTransactionAccount;
-import net.ktnx.mobileledger.utils.Logger;
-import net.ktnx.mobileledger.utils.Misc;
-
-import org.jetbrains.annotations.NotNull;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.List;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static net.ktnx.mobileledger.utils.Logger.debug;
-
-public class NewTransactionModel extends ViewModel {
-    static final Pattern reYMD = Pattern.compile("^\\s*(\\d+)\\d*/\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    static final Pattern reMD = Pattern.compile("^\\s*(\\d+)\\s*/\\s*(\\d+)\\s*$");
-    static final Pattern reD = Pattern.compile("\\s*(\\d+)\\s*$");
-    private final Item header = new Item(this, null, "");
-    private final Item trailer = new Item(this);
-    private final ArrayList<Item> items = new ArrayList<>();
-    private final MutableLiveData<Boolean> isSubmittable = new MutableLiveData<>(false);
-    private final MutableLiveData<Integer> focusedItem = new MutableLiveData<>(0);
-    private final MutableLiveData<Integer> accountCount = new MutableLiveData<>(0);
-    private final MutableLiveData<Boolean> simulateSave = new MutableLiveData<>(false);
-    public boolean getSimulateSave() {
-        return simulateSave.getValue();
-    }
-    public void setSimulateSave(boolean simulateSave) {
-        this.simulateSave.setValue(simulateSave);
-    }
-    public void toggleSimulateSave() {
-        simulateSave.setValue(!simulateSave.getValue());
-    }
-    public void observeSimulateSave(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                    @NonNull
-                                            androidx.lifecycle.Observer<? super Boolean> observer) {
-        this.simulateSave.observe(owner, observer);
-    }
-    public int getAccountCount() {
-        return items.size();
-    }
-    public Date getDate() {
-        return header.date.getValue();
-    }
-    public String getDescription() {
-        return header.description.getValue();
-    }
-    public LiveData<Boolean> isSubmittable() {
-        return this.isSubmittable;
-    }
-    void reset() {
-        header.date.setValue(null);
-        header.description.setValue(null);
-        items.clear();
-        items.add(new Item(this, new LedgerTransactionAccount("")));
-        items.add(new Item(this, new LedgerTransactionAccount("")));
-        focusedItem.setValue(0);
-    }
-    public void observeFocusedItem(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                   @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
-        this.focusedItem.observe(owner, observer);
-    }
-    public void stopObservingFocusedItem(
-            @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
-        this.focusedItem.removeObserver(observer);
-    }
-    public void observeAccountCount(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                    @NonNull
-                                            androidx.lifecycle.Observer<? super Integer> observer) {
-        this.accountCount.observe(owner, observer);
-    }
-    public void stopObservingAccountCount(
-            @NonNull androidx.lifecycle.Observer<? super Integer> observer) {
-        this.accountCount.removeObserver(observer);
-    }
-    public int getFocusedItem() { return focusedItem.getValue(); }
-    public void setFocusedItem(int position) {
-        focusedItem.setValue(position);
-    }
-    public 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();
-    }
-    public Item getItem(int index) {
-        if (index == 0) {
-            return header;
-        }
-
-        if (index <= items.size())
-            return items.get(index - 1);
-
-        return trailer;
-    }
-    /*
-     A transaction is submittable if:
-     0) has description
-     1) has at least two account names
-     2) each amount has account name
-     3) amounts must balance to 0, or
-     3a) there must be exactly one empty amount (with account)
-     4) empty accounts with empty amounts are ignored
-     5) a row with an empty account name or empty amount is guaranteed to exist
-    */
-    @SuppressLint("DefaultLocale")
-    public void checkTransactionSubmittable(NewTransactionItemsAdapter adapter) {
-        int accounts = 0;
-        int amounts = 0;
-        int empty_rows = 0;
-        float balance = 0f;
-        final String descriptionText = getDescription();
-        boolean submittable = true;
-        List<Item> itemsWithEmptyAmount = new ArrayList<>();
-        List<Item> itemsWithAccountAndEmptyAmount = new ArrayList<>();
-
-        try {
-            if ((descriptionText == null) || descriptionText.trim()
-                                                            .isEmpty())
-            {
-                Logger.debug("submittable", "Transaction not submittable: missing description");
-                submittable = false;
-            }
-
-            for (int i = 0; i < this.items.size(); i++) {
-                Item item = this.items.get(i);
-
-                LedgerTransactionAccount acc = item.getAccount();
-                String acc_name = acc.getAccountName()
-                                     .trim();
-                if (acc_name.isEmpty()) {
-                    empty_rows++;
-
-                    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 {
-                    accounts++;
-                }
-
-                if (acc.isAmountSet()) {
-                    amounts++;
-                    balance += acc.getAmount();
-                }
-                else {
-                    itemsWithEmptyAmount.add(item);
-
-                    if (!acc_name.isEmpty()) {
-                        itemsWithAccountAndEmptyAmount.add(item);
-                    }
-                }
-            }
-
-            // 1) has at least two account names
-            if (accounts < 2) {
-                Logger.debug("submittable",
-                        String.format("Transaction not submittable: only %d account names",
-                                accounts));
-                submittable = false;
-            }
-
-            // 3) amount must balance to 0, or
-            // 3a) there must be exactly one empty amount (with account)
-            if (Misc.isZero(balance)) {
-                for (Item item : items) {
-                    item.setAmountHint(null);
-                }
-            }
-            else {
-                int balanceReceiversCount = itemsWithAccountAndEmptyAmount.size();
-                if (balanceReceiversCount != 1) {
-                    Logger.debug("submittable", (balanceReceiversCount == 0) ?
-                                                "Transaction not submittable: non-zero balance " +
-                                                "with no empty amounts with accounts" :
-                                                "Transaction not submittable: non-zero balance " +
-                                                "with multiple empty amounts with accounts");
-                    submittable = false;
-                }
-
-                // suggest off-balance amount to a row and remove hints on other rows
-                Item receiver = null;
-                if (!itemsWithAccountAndEmptyAmount.isEmpty())
-                    receiver = itemsWithAccountAndEmptyAmount.get(0);
-                else if (!itemsWithEmptyAmount.isEmpty())
-                    receiver = itemsWithEmptyAmount.get(0);
-
-                for (Item item : items) {
-                    if (item.equals(receiver)) {
-                        Logger.debug("submittable",
-                                String.format("Setting amount hint to %1.2f", -balance));
-                        item.setAmountHint(String.format("%1.2f", -balance));
-                    }
-                    else
-                        item.setAmountHint(null);
-                }
-            }
-
-            // 5) a row with an empty account name or empty amount is guaranteed to exist
-            if ((empty_rows == 0) &&
-                ((this.items.size() == accounts) || (this.items.size() == amounts)))
-            {
-                adapter.addRow();
-            }
-
-
-            debug("submittable", submittable ? "YES" : "NO");
-            isSubmittable.setValue(submittable);
-
-            if (BuildConfig.DEBUG) {
-                debug("submittable", "== Dump of all items");
-                for (int i = 0; i < items.size(); i++) {
-                    Item item = items.get(i);
-                    LedgerTransactionAccount acc = item.getAccount();
-                    debug("submittable", String.format("Item %2d: [%4.2f] %s", i,
-                            acc.isAmountSet() ? acc.getAmount() : 0, acc.getAccountName()));
-                }
-            }
-        }
-        catch (NumberFormatException e) {
-            debug("submittable", "NO (because of NumberFormatException)");
-            isSubmittable.setValue(false);
-        }
-        catch (Exception e) {
-            e.printStackTrace();
-            debug("submittable", "NO (because of an Exception)");
-            isSubmittable.setValue(false);
-        }
-    }
-    public void removeItem(int pos) {
-        items.remove(pos);
-        accountCount.setValue(getAccountCount());
-    }
-    public void sendCountNotifications() {
-        accountCount.setValue(getAccountCount());
-    }
-    public void sendFocusedNotification() {
-        focusedItem.setValue(focusedItem.getValue());
-    }
-    public void updateFocusedItem(int position) {
-        focusedItem.setValue(position);
-    }
-    public void noteFocusIsOnAccount(int position) {
-        getItem(position).setFocusIsOnAmount(false);
-    }
-    public void noteFocusIsOnAmount(int position) {
-        getItem(position).setFocusIsOnAmount(true);
-    }
-    enum ItemType {generalData, transactionRow, bottomFiller}
-
-    //==========================================================================================
-
-    class Item extends Object {
-        private ItemType type;
-        private MutableLiveData<Date> date = new MutableLiveData<>();
-        private MutableLiveData<String> description = new MutableLiveData<>();
-        private LedgerTransactionAccount account;
-        private MutableLiveData<String> amountHint = new MutableLiveData<>(null);
-        private NewTransactionModel model;
-        private MutableLiveData<Boolean> editable = new MutableLiveData<>(true);
-        private boolean focusIsOnAmount = false;
-        public Item(NewTransactionModel model) {
-            this.model = model;
-            type = ItemType.bottomFiller;
-            editable.setValue(false);
-        }
-        public Item(NewTransactionModel model, Date date, String description) {
-            this.model = model;
-            this.type = ItemType.generalData;
-            this.date.setValue(date);
-            this.description.setValue(description);
-            this.editable.setValue(true);
-        }
-        public Item(NewTransactionModel model, LedgerTransactionAccount account) {
-            this.model = model;
-            this.type = ItemType.transactionRow;
-            this.account = account;
-            this.editable.setValue(true);
-        }
-        public boolean focusIsOnAmount() {
-            return focusIsOnAmount;
-        }
-        public NewTransactionModel getModel() {
-            return model;
-        }
-        public void setEditable(boolean editable) {
-            ensureType(ItemType.generalData, ItemType.transactionRow);
-            this.editable.setValue(editable);
-        }
-        private void ensureType(ItemType type1, ItemType type2) {
-            if ((type != type1) && (type != type2)) {
-                throw new RuntimeException(
-                        String.format("Actual type (%s) differs from wanted (%s or %s)", type,
-                                type1, type2));
-            }
-        }
-        public String getAmountHint() {
-            ensureType(ItemType.transactionRow);
-            return amountHint.getValue();
-        }
-        public void setAmountHint(String amountHint) {
-            ensureType(ItemType.transactionRow);
-
-            // avoid unnecessary triggers
-            if (amountHint == null) {
-                if (this.amountHint.getValue() == null)
-                    return;
-            }
-            else {
-                if (amountHint.equals(this.amountHint.getValue()))
-                    return;
-            }
-
-            this.amountHint.setValue(amountHint);
-        }
-        public void observeAmountHint(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                      @NonNull
-                                              androidx.lifecycle.Observer<? super String> observer) {
-            this.amountHint.observe(owner, observer);
-        }
-        public void stopObservingAmountHint(
-                @NonNull androidx.lifecycle.Observer<? super String> observer) {
-            this.amountHint.removeObserver(observer);
-        }
-        public ItemType getType() {
-            return type;
-        }
-        public void ensureType(ItemType wantedType) {
-            if (type != wantedType) {
-                throw new RuntimeException(
-                        String.format("Actual type (%s) differs from wanted (%s)", type,
-                                wantedType));
-            }
-        }
-        public Date getDate() {
-            ensureType(ItemType.generalData);
-            return date.getValue();
-        }
-        public void setDate(Date date) {
-            ensureType(ItemType.generalData);
-            this.date.setValue(date);
-        }
-        public void setDate(String text) {
-            if ((text == null) || text.trim()
-                                      .isEmpty())
-            {
-                setDate((Date) null);
-                return;
-            }
-
-            int year, month, day;
-            final Calendar c = GregorianCalendar.getInstance();
-            Matcher m = reYMD.matcher(text);
-            if (m.matches()) {
-                year = Integer.parseInt(m.group(1));
-                month = Integer.parseInt(m.group(2)) - 1;   // month is 0-based
-                day = Integer.parseInt(m.group(3));
-            }
-            else {
-                year = c.get(Calendar.YEAR);
-                m = reMD.matcher(text);
-                if (m.matches()) {
-                    month = Integer.parseInt(m.group(1)) - 1;
-                    day = Integer.parseInt(m.group(2));
-                }
-                else {
-                    month = c.get(Calendar.MONTH);
-                    m = reD.matcher(text);
-                    if (m.matches()) {
-                        day = Integer.parseInt(m.group(1));
-                    }
-                    else {
-                        day = c.get(Calendar.DAY_OF_MONTH);
-                    }
-                }
-            }
-
-            c.set(year, month, day);
-
-            this.setDate(c.getTime());
-        }
-        public void observeDate(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                @NonNull androidx.lifecycle.Observer<? super Date> observer) {
-            this.date.observe(owner, observer);
-        }
-        public void stopObservingDate(@NonNull androidx.lifecycle.Observer<? super Date> 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);
-        }
-        public void observeDescription(@NonNull @NotNull androidx.lifecycle.LifecycleOwner owner,
-                                       @NonNull
-                                               androidx.lifecycle.Observer<? super String> observer) {
-            this.description.observe(owner, observer);
-        }
-        public void stopObservingDescription(
-                @NonNull androidx.lifecycle.Observer<? super String> observer) {
-            this.description.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
-         */
-        public String getFormattedDate() {
-            if (date == null)
-                return null;
-            Date time = date.getValue();
-            if (time == null)
-                return null;
-
-            Calendar c = GregorianCalendar.getInstance();
-            c.setTime(time);
-            Calendar today = GregorianCalendar.getInstance();
-
-            final int myYear = c.get(Calendar.YEAR);
-            final int myMonth = c.get(Calendar.MONTH);
-            final int myDay = c.get(Calendar.DAY_OF_MONTH);
-
-            if (today.get(Calendar.YEAR) != myYear) {
-                return String.format(Locale.US, "%d/%02d/%02d", myYear, myMonth + 1, myDay);
-            }
-
-            if (today.get(Calendar.MONTH) != myMonth) {
-                return String.format(Locale.US, "%d/%02d", myMonth + 1, myDay);
-            }
-
-            return String.valueOf(myDay);
-        }
-        public void observeEditableFlag(NewTransactionActivity activity,
-                                        Observer<Boolean> observer) {
-            editable.observe(activity, observer);
-        }
-        public void stopObservingEditableFlag(Observer<Boolean> observer) {
-            editable.removeObserver(observer);
-        }
-        public void setFocusIsOnAmount(boolean flag) {
-            focusIsOnAmount = flag;
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/ProfileDetailActivity.java
deleted file mode 100644 (file)
index 4536e0d..0000000
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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 androidx.appcompat.app.ActionBar;
-import androidx.appcompat.widget.Toolbar;
-
-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.utils.Colors;
-
-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;
-    @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.getThemeId()));
-            }
-        }
-
-        super.onCreate(savedInstanceState);
-        Colors.setupTheme(this, profile);
-        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);
-            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;
-    }
-
-}
index 77a9272bf9880494b52987c65db457c08bb22711..b9726492844cfd959cb208ae6258a423fb1ebd52 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -22,16 +22,50 @@ import android.os.Bundle;
 
 import androidx.annotation.Nullable;
 
 
 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.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.MLDB;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.Locale;
 
 @SuppressLint("Registered")
 public class ProfileThemedActivity extends CrashReportingActivity {
 
 @SuppressLint("Registered")
 public class ProfileThemedActivity extends CrashReportingActivity {
-    protected MobileLedgerProfile mProfile;
-    protected void setupProfileColors() {
-        Colors.setupTheme(this, mProfile);
+    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() {
     }
     @Override
     protected void onStart() {
@@ -40,25 +74,62 @@ public class ProfileThemedActivity extends CrashReportingActivity {
     }
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         initProfile();
     }
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         initProfile();
-        super.onCreate(savedInstanceState);
 
 
-        setupProfileColors();
+        Data.observeProfile(this, profile -> {
+            if (profile == null) {
+                Logger.debug(TAG, "No current profile, leaving");
+                return;
+            }
+
+            mProfile = profile;
+            storeProfilePref(profile);
+            int hue = profile.getTheme();
 
 
-        Data.profile.observe(this, mobileLedgerProfile -> {
-            mProfile = mobileLedgerProfile;
-            setupProfileColors();
+            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() {
     }
     protected void initProfile() {
-        mProfile = Data.profile.getValue();
-        if (mProfile == null) {
-            String profileUUID = MLDB.getOption(MLDB.OPT_PROFILE_UUID, null);
-            MobileLedgerProfile startupProfile;
+        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);
 
 
-            startupProfile = Data.getProfile(profileUUID);
-            Data.setCurrentProfile(startupProfile);
-            mProfile = startupProfile;
+        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);
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SettingsActivity.java
deleted file mode 100644 (file)
index 914e161..0000000
+++ /dev/null
@@ -1,286 +0,0 @@
-/*
- * 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.annotation.TargetApi;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Configuration;
-import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Bundle;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceManager;
-import android.preference.RingtonePreference;
-import androidx.core.app.NavUtils;
-import androidx.appcompat.app.ActionBar;
-import android.text.TextUtils;
-import android.view.MenuItem;
-
-import net.ktnx.mobileledger.R;
-
-import java.util.List;
-
-/**
- * A {@link PreferenceActivity} that presents a set of application settings. On
- * handset devices, settings are presented as a single list. On tablets,
- * settings are split by category, with category headers shown to the left of
- * the list of settings.
- * <p>
- * See <a href="http://developer.android.com/design/patterns/settings.html">
- * Android Design: Settings</a> for design guidelines and the <a
- * href="http://developer.android.com/guide/topics/ui/settings.html">Settings
- * API Guide</a> for more information on developing a Settings UI.
- */
-public class SettingsActivity extends AppCompatPreferenceActivity {
-    public static String PREF_KEY_SHOW_ONLY_STARRED_ACCOUNTS = "pref_show_only_starred_accounts";
-
-    /**
-     * A preference value change listener that updates the preference's summary
-     * to reflect its new value.
-     */
-    private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
-        String stringValue = value.toString();
-
-        if (preference instanceof ListPreference) {
-            // For list preferences, look up the correct display value in
-            // the preference's 'entries' list.
-            ListPreference listPreference = (ListPreference) preference;
-            int index = listPreference.findIndexOfValue(stringValue);
-
-            // Set the summary to reflect the new value.
-            preference.setSummary(
-                    index >= 0
-                            ? listPreference.getEntries()[index]
-                            : null);
-
-        } else if (preference instanceof RingtonePreference) {
-            // For ringtone preferences, look up the correct display value
-            // using RingtoneManager.
-            if (TextUtils.isEmpty(stringValue)) {
-                // Empty values correspond to 'silent' (no ringtone).
-                preference.setSummary(R.string.pref_ringtone_silent);
-
-            } else {
-                Ringtone ringtone = RingtoneManager.getRingtone(
-                        preference.getContext(), Uri.parse(stringValue));
-
-                if (ringtone == null) {
-                    // Clear the summary if there was a lookup error.
-                    preference.setSummary(null);
-                } else {
-                    // Set the summary to reflect the new ringtone display
-                    // name.
-                    String name = ringtone.getTitle(preference.getContext());
-                    preference.setSummary(name);
-                }
-            }
-        } else {
-            // For all other preferences, set the summary to the value's
-            // simple string representation.
-            preference.setSummary(stringValue);
-        }
-
-        return true;
-    };
-
-    /**
-     * Helper method to determine if the device has an extra-large screen. For
-     * example, 10" tablets are extra-large.
-     */
-    private static boolean isXLargeTablet(Context context) {
-        return (context.getResources().getConfiguration().screenLayout
-                & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
-    }
-
-    /**
-     * Binds a preference's summary to its value. More specifically, when the
-     * preference's value is changed, its summary (line of text below the
-     * preference title) is updated to reflect the value. The summary is also
-     * immediately updated upon calling this method. The exact display format is
-     * dependent on the type of preference.
-     *
-     * @see #sBindPreferenceSummaryToValueListener
-     */
-    private static void bindPreferenceSummaryToValue(Preference preference) {
-        // Set the listener to watch for value changes.
-        preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
-
-        // Trigger the listener immediately with the preference's
-        // current value.
-        sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
-                PreferenceManager
-                        .getDefaultSharedPreferences(preference.getContext())
-                        .getString(preference.getKey(), ""));
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setupActionBar();
-    }
-
-    /**
-     * Set up the {@link android.app.ActionBar}, if the API is available.
-     */
-    private void setupActionBar() {
-        ActionBar actionBar = getSupportActionBar();
-        if (actionBar != null) {
-            // Show the Up button in the action bar.
-            actionBar.setDisplayHomeAsUpEnabled(true);
-        }
-    }
-
-    @Override
-    public boolean onMenuItemSelected(int featureId, MenuItem item) {
-        int id = item.getItemId();
-        if (id == android.R.id.home) {
-            if (!super.onMenuItemSelected(featureId, item)) {
-                NavUtils.navigateUpFromSameTask(this);
-            }
-            return true;
-        }
-        return super.onMenuItemSelected(featureId, item);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean onIsMultiPane() {
-        return isXLargeTablet(this);
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public void onBuildHeaders(List<Header> target) {
-        loadHeadersFromResource(R.xml.pref_headers, target);
-    }
-
-    /**
-     * This method stops fragment injection in malicious applications.
-     * Make sure to deny any unknown fragments here.
-     */
-    protected boolean isValidFragment(String fragmentName) {
-        return PreferenceFragment.class.getName().equals(fragmentName)
-                || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
-                || NotificationPreferenceFragment.class.getName().equals(fragmentName)
-                || InterfacePreferenceFragment.class.getName().equals(fragmentName);
-    }
-
-    /**
-     * This fragment shows general preferences only. It is used when the
-     * activity is showing a two-pane settings UI.
-     */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public static
-    class InterfacePreferenceFragment extends PreferenceFragment {
-        @Override
-        public
-        void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            addPreferencesFromResource(R.xml.pref_interface);
-            setHasOptionsMenu(true);
-
-            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
-            // to their values. When their values change, their summaries are
-            // updated to reflect the new value, per the Android Design
-            // guidelines.
-
-        }
-
-        @Override
-        public
-        boolean onOptionsItemSelected(MenuItem item) {
-            int id = item.getItemId();
-            if (id == android.R.id.home) {
-                startActivity(new Intent(getActivity(), SettingsActivity.class));
-                return true;
-            }
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    /**
-     * This fragment shows notification preferences only. It is used when the
-     * activity is showing a two-pane settings UI.
-     */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public static class NotificationPreferenceFragment extends PreferenceFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            addPreferencesFromResource(R.xml.pref_notification);
-            setHasOptionsMenu(true);
-
-            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
-            // to their values. When their values change, their summaries are
-            // updated to reflect the new value, per the Android Design
-            // guidelines.
-            bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
-        }
-
-        @Override
-        public boolean onOptionsItemSelected(MenuItem item) {
-            int id = item.getItemId();
-            if (id == android.R.id.home) {
-                startActivity(new Intent(getActivity(), SettingsActivity.class));
-                return true;
-            }
-            return super.onOptionsItemSelected(item);
-        }
-    }
-
-    /**
-     * This fragment shows data and sync preferences only. It is used when the
-     * activity is showing a two-pane settings UI.
-     */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
-    public static class DataSyncPreferenceFragment extends PreferenceFragment {
-        @Override
-        public void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            addPreferencesFromResource(R.xml.pref_data_sync);
-            setHasOptionsMenu(true);
-
-            // Bind the summaries of EditText/List/Dialog/Ringtone preferences
-            // to their values. When their values change, their summaries are
-            // updated to reflect the new value, per the Android Design
-            // guidelines.
-            bindPreferenceSummaryToValue(findPreference("sync_frequency"));
-        }
-
-        @Override
-        public boolean onOptionsItemSelected(MenuItem item) {
-            int id = item.getItemId();
-            if (id == android.R.id.home) {
-                startActivity(new Intent(getActivity(), SettingsActivity.class));
-                return true;
-            }
-            return super.onOptionsItemSelected(item);
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/activity/SplashActivity.java
new file mode 100644 (file)
index 0000000..3fe2048
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.activity;
+
+import android.content.Intent;
+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.db.DB;
+import net.ktnx.mobileledger.utils.Logger;
+
+import java.util.Locale;
+
+public class SplashActivity extends CrashReportingActivity {
+    private static final long keepActiveForMS = 400;
+    private long startupTime;
+    private boolean running = true;
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setTheme(R.style.AppTheme_default);
+        setContentView(R.layout.splash_activity_layout);
+        Logger.debug("splash", "onCreate()");
+
+        DB.initComplete.setValue(false);
+        DB.initComplete.observe(this, this::onDbInitDoneChanged);
+    }
+    @Override
+    protected void onStart() {
+        super.onStart();
+        Logger.debug("splash", "onStart()");
+        running = true;
+
+        startupTime = System.currentTimeMillis();
+
+        DatabaseInitThread dbInitThread = new DatabaseInitThread();
+        Logger.debug("splash", "starting dbInit task");
+        dbInitThread.start();
+    }
+    @Override
+    protected void onPause() {
+        super.onPause();
+        Logger.debug("splash", "onPause()");
+        running = false;
+    }
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Logger.debug("splash", "onResume()");
+        running = true;
+    }
+    private void onDbInitDoneChanged(Boolean done) {
+        if (!done) {
+            Logger.debug("splash", "DB not yet initialized");
+            return;
+        }
+
+        Logger.debug("splash", "DB init done");
+        long now = System.currentTimeMillis();
+        if (now > startupTime + keepActiveForMS)
+            startMainActivity();
+        else {
+            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() {
+        if (running) {
+            Logger.debug("splash", "still running, launching main activity");
+            Intent intent = new Intent(this, MainActivity.class);
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION |
+                            Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivity(intent);
+            overridePendingTransition(R.anim.fade_in_slowly, R.anim.fade_out_slowly);
+        }
+        else {
+            Logger.debug("splash", "Not running, finish and go away");
+            finish();
+        }
+    }
+    private static class DatabaseInitThread extends Thread {
+        @Override
+        public void run() {
+            long ignored = DB.get()
+                             .getProfileDAO()
+                             .getProfileCountSync();
+
+            DB.initComplete.postValue(true);
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionAccountRowItemHolder.java
new file mode 100644 (file)
index 0000000..8f1c51d
--- /dev/null
@@ -0,0 +1,538 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionActivity.java
new file mode 100644 (file)
index 0000000..a1d9e74
--- /dev/null
@@ -0,0 +1,408 @@
+/*
+ * 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();
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionFragment.java
new file mode 100644 (file)
index 0000000..ec9fb39
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionHeaderItemHolder.java
new file mode 100644 (file)
index 0000000..44464c8
--- /dev/null
@@ -0,0 +1,368 @@
+/*
+ * 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());
+
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemViewHolder.java
new file mode 100644 (file)
index 0000000..25e0bb6
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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);
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionItemsAdapter.java
new file mode 100644 (file)
index 0000000..b6b17be
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/new_transaction/NewTransactionModel.java
new file mode 100644 (file)
index 0000000..51bcd30
--- /dev/null
@@ -0,0 +1,1366 @@
+/*
+ * 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();
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailActivity.java
new file mode 100644 (file)
index 0000000..42c8490
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * 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);
+    }
+}
index 8ec562b6b5cee139ef00bade5ba2f4206f0b739a..99934543e7b544bf876fcc0010282d9cb373801e 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,6 +19,8 @@ package net.ktnx.mobileledger.ui.profiles;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 
 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.os.Bundle;
 import android.text.Editable;
 import android.text.TextWatcher;
@@ -28,34 +30,41 @@ import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.LinearLayout;
 import android.widget.PopupMenu;
 import android.widget.PopupMenu;
-import android.widget.Switch;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
+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 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.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.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.HueRingDialog;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
 import net.ktnx.mobileledger.utils.Colors;
 import net.ktnx.mobileledger.utils.Colors;
+import net.ktnx.mobileledger.utils.Misc;
 
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 
 import java.net.MalformedURLException;
 import java.net.URL;
 
 import org.jetbrains.annotations.NonNls;
 import org.jetbrains.annotations.NotNull;
 
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 import java.util.Objects;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
@@ -65,43 +74,24 @@ import static net.ktnx.mobileledger.utils.Logger.debug;
  * a {@link ProfileDetailActivity}
  * on handsets.
  */
  * a {@link ProfileDetailActivity}
  * on handsets.
  */
-public class ProfileDetailFragment extends Fragment implements HueRingDialog.HueSelectedListener {
+public class ProfileDetailFragment extends Fragment {
     /**
      * The fragment argument representing the item ID that this fragment
      * represents.
      */
     public static final String ARG_ITEM_ID = "item_id";
     /**
      * The fragment argument representing the item ID that this fragment
      * represents.
      */
     public static final String ARG_ITEM_ID = "item_id";
+    public static final String ARG_HUE = "hue";
     @NonNls
     @NonNls
-    private static final String HTTPS_URL_START = "https://";
-
-    /**
-     * The dummy content this fragment is presenting.
-     */
-    private MobileLedgerProfile mProfile;
-    private TextView url;
-    private Switch postingPermitted;
-    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 TextInputLayout preferredAccountsFilterLayout;
-    private View huePickerView;
-    private View insecureWarningText;
-    private TextView futureDatesText;
-    private MobileLedgerProfile.FutureDates futureDates;
-    private View futureDatesLayout;
 
 
+    private boolean defaultCommoditySet;
+    private boolean syncingModelFromUI = false;
+    private ProfileDetailBinding binding;
     /**
      * Mandatory empty constructor for the fragment manager to instantiate the
      * fragment (e.g. upon screen orientation changes).
      */
     public ProfileDetailFragment() {
     /**
      * Mandatory empty constructor for the fragment manager to instantiate the
      * fragment (e.g. upon screen orientation changes).
      */
     public ProfileDetailFragment() {
+        super(R.layout.profile_detail);
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
@@ -109,279 +99,335 @@ public class ProfileDetailFragment extends Fragment implements HueRingDialog.Hue
         super.onCreateOptionsMenu(menu, inflater);
         inflater.inflate(R.menu.profile_details, menu);
         final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete);
         super.onCreateOptionsMenu(menu, inflater);
         inflater.inflate(R.menu.profile_details, menu);
         final MenuItem menuDeleteProfile = menu.findItem(R.id.menuDelete);
-        menuDeleteProfile.setOnMenuItemClickListener(item -> {
-            AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
-            builder.setTitle(mProfile.getName());
-            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.profile.getValue())) {
-                    debug("profiles", "[fragment] setting current profile to 0");
-                    Data.setCurrentProfile(newList.get(0));
-                }
-
-                final FragmentActivity activity = getActivity();
-                if (activity != null)
-                    activity.finish();
-            });
-            builder.show();
-            return false;
-        });
-        final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
-        menuDeleteProfile.setVisible(
-                (mProfile != null) && (profiles != null) && (profiles.size() > 1));
+        menuDeleteProfile.setOnMenuItemClickListener(item -> onDeleteProfile());
+        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.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());
+        @NotNull ProfileDetailModel model = getModel();
+        builder.setTitle(model.getProfileName());
+        builder.setMessage(R.string.remove_profile_dialog_message);
+        builder.setPositiveButton(R.string.Remove, (dialog, which) -> {
+            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)
+                activity.finish();
+        });
+        builder.show();
+        return false;
     }
     private boolean onWipeDataMenuClicked() {
         // this is a development option, so no confirmation
     }
     private boolean onWipeDataMenuClicked() {
         // this is a development option, so no confirmation
-        mProfile.wipeAllData();
-        if (mProfile.equals(Data.profile.getValue()))
-            triggerProfileChange();
+        DB.get()
+          .getProfileDAO()
+          .getById(Objects.requireNonNull(getModel().getProfileId()
+                                                    .getValue()))
+          .observe(getViewLifecycleOwner(), profile -> {
+              if (profile != null)
+                  profile.wipeAllData();
+          });
         return true;
     }
         return true;
     }
-    private void triggerProfileChange() {
-        int index = Data.getProfileIndex(mProfile);
-        MobileLedgerProfile newProfile = new MobileLedgerProfile(mProfile);
-        final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
-        if (profiles == null)
-            throw new AssertionError();
-        profiles.set(index, newProfile);
-
-        ProfilesRecyclerViewAdapter prva = ProfilesRecyclerViewAdapter.getInstance();
-        if (prva != null)
-            prva.notifyItemChanged(index);
-
-        if (mProfile.equals(Data.profile.getValue()))
-            Data.profile.setValue(newProfile);
+    private void hookTextChangeSyncRoutine(TextView view, TextChangeSyncRoutine syncRoutine) {
+        view.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) { syncRoutine.onTextChanged(s.toString());}
+        });
     }
     }
+    @Nullable
     @Override
     @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;
 
         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);
+            else
+                resetDefaultCommodity();
+        });
 
 
-        FloatingActionButton fab = context.findViewById(R.id.fab);
+        FloatingActionButton fab = context.findViewById(R.id.fabAdd);
         fab.setOnClickListener(v -> onSaveFabClicked());
         fab.setOnClickListener(v -> onSaveFabClicked());
-        profileName = context.findViewById(R.id.profile_name);
-        profileNameLayout = context.findViewById(R.id.profile_name_layout);
-        url = context.findViewById(R.id.url);
-        urlLayout = context.findViewById(R.id.url_layout);
-        postingPermitted = context.findViewById(R.id.profile_permit_posting);
-        futureDatesLayout = context.findViewById(R.id.future_dates_layout);
-        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 -> {
-                       switch (item.getItemId()) {
-                           case R.id.menu_future_dates_30:
-                               futureDates = MobileLedgerProfile.FutureDates.OneMonth;
-                               break;
-                           case R.id.menu_future_dates_60:
-                               futureDates = MobileLedgerProfile.FutureDates.TwoMonths;
-                               break;
-                           case R.id.menu_future_dates_90:
-                               futureDates = MobileLedgerProfile.FutureDates.ThreeMonths;
-                               break;
-                           case R.id.menu_future_dates_180:
-                               futureDates = MobileLedgerProfile.FutureDates.SixMonths;
-                               break;
-                           case R.id.menu_future_dates_365:
-                               futureDates = MobileLedgerProfile.FutureDates.OneYear;
-                               break;
-                           case R.id.menu_future_dates_all:
-                               futureDates = MobileLedgerProfile.FutureDates.All;
-                               break;
-                           default:
-                               futureDates = MobileLedgerProfile.FutureDates.None;
-                       }
-                       futureDatesText.setText(futureDates.getText(getResources()));
-                       return true;
-                   });
-                   menu.show();
-               });
-        authParams = context.findViewById(R.id.auth_params);
-        useAuthentication = context.findViewById(R.id.enable_http_auth);
-        userName = context.findViewById(R.id.auth_user_name);
-        userNameLayout = context.findViewById(R.id.auth_user_name_layout);
-        password = context.findViewById(R.id.password);
-        passwordLayout = context.findViewById(R.id.password_layout);
-        huePickerView = context.findViewById(R.id.btn_pick_ring_color);
-        preferredAccountsFilter = context.findViewById(R.id.preferred_accounts_filter_filter);
-        preferredAccountsFilterLayout =
-                context.findViewById(R.id.preferred_accounts_accounts_filter_layout);
-        insecureWarningText = context.findViewById(R.id.insecure_scheme_text);
-
-        useAuthentication.setOnCheckedChangeListener((buttonView, isChecked) -> {
-            debug("profiles", isChecked ? "auth enabled " : "auth disabled");
-            authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
-            if (isChecked)
-                userName.requestFocus();
+
+        hookTextChangeSyncRoutine(binding.profileName, model::setProfileName);
+        model.observeProfileName(viewLifecycleOwner, pn -> {
+            if (!Misc.equalStrings(pn, Misc.nullIsEmpty(binding.profileName.getText())))
+                binding.profileName.setText(pn);
+        });
+
+        hookTextChangeSyncRoutine(binding.url, model::setUrl);
+        model.observeUrl(viewLifecycleOwner, u -> {
+            if (!Misc.equalStrings(u, Misc.nullIsEmpty(binding.url.getText())))
+                binding.url.setText(u);
+        });
+
+        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");
+        });
+
+        binding.profileShowCommodity.setOnCheckedChangeListener(
+                (buttonView, isChecked) -> model.setShowCommodityByDefault(isChecked));
+        model.observeShowCommodityByDefault(viewLifecycleOwner,
+                binding.profileShowCommodity::setChecked);
+
+        model.observePostingPermitted(viewLifecycleOwner, isChecked -> {
+            binding.profilePermitPosting.setChecked(isChecked);
+            binding.postingSubItems.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+        });
+        binding.profilePermitPosting.setOnCheckedChangeListener(
+                ((buttonView, isChecked) -> model.setPostingPermitted(isChecked)));
+
+        model.observeShowCommentsByDefault(viewLifecycleOwner,
+                binding.profileShowComments::setChecked);
+        binding.profileShowComments.setOnCheckedChangeListener(
+                ((buttonView, isChecked) -> model.setShowCommentsByDefault(isChecked)));
+
+        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 -> binding.futureDatesText.setText(v.getText(getResources())));
+
+        model.observeApiVersion(viewLifecycleOwner,
+                apiVer -> binding.apiVersionText.setText(apiVer.getDescription(getResources())));
+        binding.apiVersionLabel.setOnClickListener(this::chooseAPIVersion);
+        binding.apiVersionText.setOnClickListener(this::chooseAPIVersion);
+
+        binding.serverVersionLabel.setOnClickListener(v -> model.triggerVersionDetection());
+        model.observeDetectedVersion(viewLifecycleOwner, ver -> {
+            if (ver == null)
+                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
+                binding.detectedServerVersionText.setText(ver.toString());
+        });
+        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 (!wasOn && isChecked)
+                binding.authUserName.requestFocus();
+        });
+        model.observeUseAuthentication(viewLifecycleOwner, isChecked -> {
+            binding.enableHttpAuth.setChecked(isChecked);
+            binding.authParams.setVisibility(isChecked ? View.VISIBLE : View.GONE);
             checkInsecureSchemeWithAuth();
         });
 
             checkInsecureSchemeWithAuth();
         });
 
-        postingPermitted.setOnCheckedChangeListener(((buttonView, isChecked) -> {
-            preferredAccountsFilterLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
-            futureDatesLayout.setVisibility(isChecked ? View.VISIBLE : View.GONE);
-        }));
-
-        hookClearErrorOnFocusListener(profileName, profileNameLayout);
-        hookClearErrorOnFocusListener(url, urlLayout);
-        hookClearErrorOnFocusListener(userName, userNameLayout);
-        hookClearErrorOnFocusListener(password, passwordLayout);
-
-        int profileThemeId;
-        if (mProfile != null) {
-            profileName.setText(mProfile.getName());
-            postingPermitted.setChecked(mProfile.isPostingPermitted());
-            futureDates = mProfile.getFutureDates();
-            futureDatesText.setText(futureDates.getText(getResources()));
-            url.setText(mProfile.getUrl());
-            useAuthentication.setChecked(mProfile.isAuthEnabled());
-            authParams.setVisibility(mProfile.isAuthEnabled() ? View.VISIBLE : View.GONE);
-            userName.setText(mProfile.isAuthEnabled() ? mProfile.getAuthUserName() : "");
-            password.setText(mProfile.isAuthEnabled() ? mProfile.getAuthPassword() : "");
-            preferredAccountsFilter.setText(mProfile.getPreferredAccountsFilter());
-            profileThemeId = mProfile.getThemeId();
-        }
-        else {
-            profileName.setText("");
-            url.setText(HTTPS_URL_START);
-            postingPermitted.setChecked(true);
-            futureDates = MobileLedgerProfile.FutureDates.None;
-            futureDatesText.setText(futureDates.getText(getResources()));
-            useAuthentication.setChecked(false);
-            authParams.setVisibility(View.GONE);
-            userName.setText("");
-            password.setText("");
-            preferredAccountsFilter.setText(null);
-            profileThemeId = -1;
-        }
+        model.observeUserName(viewLifecycleOwner, text -> {
+            if (!Misc.equalStrings(text, Misc.nullIsEmpty(binding.authUserName.getText())))
+                binding.authUserName.setText(text);
+        });
+        hookTextChangeSyncRoutine(binding.authUserName, model::setAuthUserName);
 
 
-        checkInsecureSchemeWithAuth();
+        model.observePassword(viewLifecycleOwner, text -> {
+            if (!Misc.equalStrings(text, Misc.nullIsEmpty(binding.password.getText())))
+                binding.password.setText(text);
+        });
+        hookTextChangeSyncRoutine(binding.password, model::setAuthPassword);
 
 
-        url.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        model.observeThemeId(viewLifecycleOwner, themeId -> {
+            final int hue = (themeId == -1) ? Colors.DEFAULT_HUE_DEG : themeId;
+            final int profileColor = Colors.getPrimaryColorForHue(hue);
+            binding.btnPickRingColor.setBackgroundColor(profileColor);
+            binding.btnPickRingColor.setTag(hue);
+        });
 
 
-            }
-            @Override
-            public void onTextChanged(CharSequence s, int start, int before, int count) {
+        model.observePreferredAccountsFilter(viewLifecycleOwner, text -> {
+            if (!Misc.equalStrings(text,
+                    Misc.nullIsEmpty(binding.preferredAccountsFilter.getText())))
+                binding.preferredAccountsFilter.setText(text);
+        });
+        hookTextChangeSyncRoutine(binding.preferredAccountsFilter,
+                model::setPreferredAccountsFilter);
 
 
-            }
+        hookClearErrorOnFocusListener(binding.profileName, binding.profileNameLayout);
+        hookClearErrorOnFocusListener(binding.url, binding.urlLayout);
+        hookClearErrorOnFocusListener(binding.authUserName, binding.authUserNameLayout);
+        hookClearErrorOnFocusListener(binding.password, binding.passwordLayout);
+
+        binding.url.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) {
                 checkInsecureSchemeWithAuth();
             }
         });
 
             @Override
             public void afterTextChanged(Editable s) {
                 checkInsecureSchemeWithAuth();
             }
         });
 
-        final int hue = (profileThemeId == -1) ? Colors.DEFAULT_HUE_DEG : profileThemeId;
-        final int profileColor = Colors.getPrimaryColorForHue(hue);
-
-        huePickerView.setBackgroundColor(profileColor);
-        huePickerView.setTag(profileThemeId);
-        huePickerView.setOnClickListener(v -> {
-            HueRingDialog d = new HueRingDialog(
-                    Objects.requireNonNull(ProfileDetailFragment.this.getContext()), profileThemeId,
-                    (Integer) v.getTag());
+        binding.btnPickRingColor.setOnClickListener(v -> {
+            HueRingDialog d = new HueRingDialog(ProfileDetailFragment.this.requireContext(),
+                    model.initialThemeHue, (Integer) v.getTag());
             d.show();
             d.show();
-            d.setColorSelectedListener(this);
+            d.setColorSelectedListener(model::setThemeId);
         });
 
         });
 
-        profileName.requestFocus();
+        binding.profileName.requestFocus();
+    }
+    private void chooseAPIVersion(View v) {
+        Activity context = getActivity();
+        ProfileDetailModel model = getModel();
+        MenuInflater mi = new MenuInflater(context);
+        PopupMenu menu = new PopupMenu(context, v);
+        menu.inflate(R.menu.api_version);
+        menu.setOnMenuItemClickListener(item -> {
+            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);
+            binding.apiVersionText.setText(apiVer.getDescription(getResources()));
+            return true;
+        });
+        menu.show();
+    }
+    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 new ViewModelProvider(requireActivity()).get(ProfileDetailModel.class);
     }
     private void onSaveFabClicked() {
         if (!checkValidity())
             return;
 
     }
     private void onSaveFabClicked() {
         if (!checkValidity())
             return;
 
-        if (mProfile != null) {
-            updateProfileFromUI();
-//                debug("profiles", String.format("Selected item is %d", mProfile.getThemeId()));
-            mProfile.storeInDB();
+        ProfileDetailModel model = getModel();
+        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");
             debug("profiles", "profile stored in DB");
-            triggerProfileChange();
+//                debug("profiles", String.format("Selected item is %d", mProfile.getThemeHue()));
         }
         else {
         }
         else {
-            mProfile = new MobileLedgerProfile();
-            updateProfileFromUI();
-            mProfile.storeInDB();
-            final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
-            if (profiles == null)
-                throw new AssertionError();
-            ArrayList<MobileLedgerProfile> newList = new ArrayList<>(profiles);
-            newList.add(mProfile);
-            Data.profiles.setValue(newList);
-            MobileLedgerProfile.storeProfilesOrder();
-
-            // first profile ever?
-            if (newList.size() == 1)
-                Data.profile.setValue(mProfile);
+            dao.insertLast(profile, null);
         }
 
         }
 
+        BackupManager.dataChanged(BuildConfig.APPLICATION_ID);
+
         Activity activity = getActivity();
         if (activity != null)
             activity.finish();
     }
         Activity activity = getActivity();
         if (activity != null)
             activity.finish();
     }
-    private void updateProfileFromUI() {
-        mProfile.setName(profileName.getText());
-        mProfile.setUrl(url.getText());
-        mProfile.setPostingPermitted(postingPermitted.isChecked());
-        mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getText());
-        mProfile.setAuthEnabled(useAuthentication.isChecked());
-        mProfile.setAuthUserName(userName.getText());
-        mProfile.setAuthPassword(password.getText());
-        mProfile.setThemeId(huePickerView.getTag());
-        mProfile.setFutureDates(futureDates);
-    }
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState) {
-        View rootView = inflater.inflate(R.layout.profile_detail, container, false);
-
-        return rootView;
-    }
     private boolean checkUrlValidity() {
         boolean valid = true;
 
     private boolean checkUrlValidity() {
         boolean valid = true;
 
-        String val = String.valueOf(url.getText())
-                           .trim();
+        ProfileDetailModel model = getModel();
+
+        String val = model.getUrl()
+                          .trim();
         if (val.isEmpty()) {
             valid = false;
         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);
         }
         try {
             URL url = new URL(val);
@@ -392,12 +438,12 @@ public class ProfileDetailFragment extends Fragment implements HueRingDialog.Hue
                                  .toUpperCase();
             if (!protocol.equals("HTTP") && !protocol.equals("HTTPS")) {
                 valid = false;
                                  .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;
             }
         }
         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;
         }
 
         return valid;
@@ -405,17 +451,19 @@ public class ProfileDetailFragment extends Fragment implements HueRingDialog.Hue
     private void checkInsecureSchemeWithAuth() {
         boolean showWarning = false;
 
     private void checkInsecureSchemeWithAuth() {
         boolean showWarning = false;
 
-        if (useAuthentication.isChecked()) {
-            String urlText = url.getText()
-                                .toString();
-            if (urlText.startsWith("http") && !urlText.startsWith("https"))
+        final ProfileDetailModel model = getModel();
+
+        if (model.getUseAuthentication()) {
+            String urlText = model.getUrl();
+            if (urlText.startsWith("http://") ||
+                urlText.length() >= 8 && !urlText.startsWith("https://"))
                 showWarning = true;
         }
 
         if (showWarning)
                 showWarning = true;
         }
 
         if (showWarning)
-            insecureWarningText.setVisibility(View.VISIBLE);
+            binding.insecureSchemeText.setVisibility(View.VISIBLE);
         else
         else
-            insecureWarningText.setVisibility(View.GONE);
+            binding.insecureSchemeText.setVisibility(View.GONE);
     }
     private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
         view.setOnFocusChangeListener((v, hasFocus) -> {
     }
     private void hookClearErrorOnFocusListener(TextView view, TextInputLayout layout) {
         view.setOnFocusChangeListener((v, hasFocus) -> {
@@ -435,45 +483,74 @@ public class ProfileDetailFragment extends Fragment implements HueRingDialog.Hue
             }
         });
     }
             }
         });
     }
+    private void syncModelFromUI() {
+        if (syncingModelFromUI)
+            return;
+
+        syncingModelFromUI = true;
+
+        try {
+            ProfileDetailModel model = getModel();
+
+            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;
 
     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;
         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 (!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;
             if (val.trim()
                    .isEmpty())
             {
                 valid = false;
-                userNameLayout.setError(
+                binding.authUserNameLayout.setError(
                         getResources().getText(R.string.err_profile_user_name_empty));
             }
 
                         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;
             if (val.trim()
                    .isEmpty())
             {
                 valid = false;
-                passwordLayout.setError(
+                binding.passwordLayout.setError(
                         getResources().getText(R.string.err_profile_password_empty));
             }
         }
 
         return valid;
     }
                         getResources().getText(R.string.err_profile_password_empty));
             }
         }
 
         return valid;
     }
-    @Override
-    public void onHueSelected(int hue) {
-        huePickerView.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
-        huePickerView.setTag(hue);
+    private void resetDefaultCommodity() {
+        defaultCommoditySet = false;
+        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;
+        binding.defaultCommodityText.setText(name);
+        binding.defaultCommodityText.setTypeface(Typeface.DEFAULT);
+    }
+    interface TextChangeSyncRoutine {
+        void onTextChanged(String text);
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/profiles/ProfileDetailModel.java
new file mode 100644 (file)
index 0000000..09b0a06
--- /dev/null
@@ -0,0 +1,387 @@
+/*
+ * 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.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.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.utils.Colors;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+import net.ktnx.mobileledger.utils.NetworkUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.util.Locale;
+import java.util.Objects;
+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<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<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() {
+    }
+    String getProfileName() {
+        return profileName.getValue();
+    }
+    void setProfileName(String newValue) {
+        if (!Misc.nullIsEmpty(newValue)
+                 .equals(Misc.nullIsEmpty(profileName.getValue())))
+            profileName.setValue(newValue);
+    }
+    void setProfileName(CharSequence newValue) {
+        setProfileName(String.valueOf(newValue));
+    }
+    void observeProfileName(LifecycleOwner lfo, Observer<String> o) {
+        profileName.observe(lfo, o);
+    }
+    Boolean getPostingPermitted() {
+        return postingPermitted.getValue();
+    }
+    void setPostingPermitted(boolean newValue) {
+        if (newValue != postingPermitted.getValue())
+            postingPermitted.setValue(newValue);
+    }
+    void observePostingPermitted(LifecycleOwner lfo, Observer<Boolean> o) {
+        postingPermitted.observe(lfo, o);
+    }
+    public void setShowCommentsByDefault(boolean newValue) {
+        if (newValue != showCommentsByDefault.getValue())
+            showCommentsByDefault.setValue(newValue);
+    }
+    void observeShowCommentsByDefault(LifecycleOwner lfo, Observer<Boolean> o) {
+        showCommentsByDefault.observe(lfo, o);
+    }
+    FutureDates getFutureDates() {
+        return futureDates.getValue();
+    }
+    void setFutureDates(FutureDates newValue) {
+        if (newValue != futureDates.getValue())
+            futureDates.setValue(newValue);
+    }
+    void observeFutureDates(LifecycleOwner lfo, Observer<FutureDates> o) {
+        futureDates.observe(lfo, o);
+    }
+    String getDefaultCommodity() {
+        return defaultCommodity.getValue();
+    }
+    void setDefaultCommodity(String newValue) {
+        if (!Misc.equalStrings(newValue, defaultCommodity.getValue()))
+            defaultCommodity.setValue(newValue);
+    }
+    void observeDefaultCommodity(LifecycleOwner lfo, Observer<String> o) {
+        defaultCommodity.observe(lfo, o);
+    }
+    Boolean getShowCommodityByDefault() {
+        return showCommodityByDefault.getValue();
+    }
+    void setShowCommodityByDefault(boolean newValue) {
+        if (newValue != showCommodityByDefault.getValue())
+            showCommodityByDefault.setValue(newValue);
+    }
+    void observeShowCommodityByDefault(LifecycleOwner lfo, Observer<Boolean> o) {
+        showCommodityByDefault.observe(lfo, o);
+    }
+    public Boolean getUseAuthentication() {
+        return useAuthentication.getValue();
+    }
+    void setUseAuthentication(boolean newValue) {
+        if (newValue != useAuthentication.getValue())
+            useAuthentication.setValue(newValue);
+    }
+    void observeUseAuthentication(LifecycleOwner lfo, Observer<Boolean> o) {
+        useAuthentication.observe(lfo, o);
+    }
+    API getApiVersion() {
+        return apiVersion.getValue();
+    }
+    void setApiVersion(API newValue) {
+        if (newValue != apiVersion.getValue())
+            apiVersion.setValue(newValue);
+    }
+    void observeApiVersion(LifecycleOwner lfo, Observer<API> o) {
+        apiVersion.observe(lfo, o);
+    }
+    HledgerVersion getDetectedVersion() { return detectedVersion.getValue(); }
+    void setDetectedVersion(HledgerVersion newValue) {
+        if (!Objects.equals(detectedVersion.getValue(), newValue))
+            detectedVersion.setValue(newValue);
+    }
+    void observeDetectedVersion(LifecycleOwner lfo, Observer<HledgerVersion> o) {
+        detectedVersion.observe(lfo, o);
+    }
+    public String getUrl() {
+        return url.getValue();
+    }
+    void setUrl(String newValue) {
+        if (!Misc.nullIsEmpty(newValue)
+                 .equals(Misc.nullIsEmpty(url.getValue())))
+            url.setValue(newValue);
+    }
+    void setUrl(CharSequence newValue) {
+        setUrl(String.valueOf(newValue));
+    }
+    void observeUrl(LifecycleOwner lfo, Observer<String> o) {
+        url.observe(lfo, o);
+    }
+    public String getAuthUserName() {
+        return authUserName.getValue();
+    }
+    void setAuthUserName(String newValue) {
+        if (!Misc.nullIsEmpty(newValue)
+                 .equals(Misc.nullIsEmpty(authUserName.getValue())))
+            authUserName.setValue(newValue);
+    }
+    void setAuthUserName(CharSequence newValue) {
+        setAuthUserName(String.valueOf(newValue));
+    }
+    void observeUserName(LifecycleOwner lfo, Observer<String> o) {
+        authUserName.observe(lfo, o);
+    }
+    public String getAuthPassword() {
+        return authPassword.getValue();
+    }
+    void setAuthPassword(String newValue) {
+        if (!Misc.nullIsEmpty(newValue)
+                 .equals(Misc.nullIsEmpty(authPassword.getValue())))
+            authPassword.setValue(newValue);
+    }
+    void setAuthPassword(CharSequence newValue) {
+        setAuthPassword(String.valueOf(newValue));
+    }
+    void observePassword(LifecycleOwner lfo, Observer<String> o) {
+        authPassword.observe(lfo, o);
+    }
+    String getPreferredAccountsFilter() {
+        return preferredAccountsFilter.getValue();
+    }
+    void setPreferredAccountsFilter(String newValue) {
+        if (!Misc.nullIsEmpty(newValue)
+                 .equals(Misc.nullIsEmpty(preferredAccountsFilter.getValue())))
+            preferredAccountsFilter.setValue(newValue);
+    }
+    void setPreferredAccountsFilter(CharSequence newValue) {
+        setPreferredAccountsFilter(String.valueOf(newValue));
+    }
+    void observePreferredAccountsFilter(LifecycleOwner lfo, Observer<String> o) {
+        preferredAccountsFilter.observe(lfo, o);
+    }
+    int getThemeId() {
+        return themeId.getValue();
+    }
+    void setThemeId(int newValue) {
+        themeId.setValue(newValue);
+    }
+    void observeThemeId(LifecycleOwner lfo, Observer<Integer> o) {
+        themeId.observe(lfo, o);
+    }
+    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());
+            orderNo.setValue(mProfile.getOrderNo());
+            postingPermitted.setValue(mProfile.permitPosting());
+            showCommentsByDefault.setValue(mProfile.getShowCommentsByDefault());
+            showCommodityByDefault.setValue(mProfile.getShowCommodityByDefault());
+            {
+                String comm = mProfile.getDefaultCommodity();
+                if (TextUtils.isEmpty(comm))
+                    setDefaultCommodity(null);
+                else
+                    setDefaultCommodity(comm);
+            }
+            futureDates.setValue(FutureDates.valueOf(mProfile.getFutureDates()));
+            apiVersion.setValue(API.valueOf(mProfile.getApiVersion()));
+            url.setValue(mProfile.getUrl());
+            useAuthentication.setValue(mProfile.useAuthentication());
+            authUserName.setValue(mProfile.useAuthentication() ? mProfile.getAuthUser() : "");
+            authPassword.setValue(mProfile.useAuthentication() ? mProfile.getAuthPassword() : "");
+            preferredAccountsFilter.setValue(mProfile.getPreferredAccountsFilter());
+            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(FutureDates.None);
+            setApiVersion(API.auto);
+            useAuthentication.setValue(false);
+            authUserName.setValue("");
+            authPassword.setValue("");
+            preferredAccountsFilter.setValue(null);
+            detectedVersion.setValue(null);
+        }
+    }
+    void updateProfile(Profile mProfile) {
+        mProfile.setId(profileId.getValue());
+        mProfile.setName(profileName.getValue());
+        mProfile.setOrderNo(orderNo.getValue());
+        mProfile.setUrl(url.getValue());
+        mProfile.setPermitPosting(postingPermitted.getValue());
+        mProfile.setShowCommentsByDefault(showCommentsByDefault.getValue());
+        mProfile.setDefaultCommodity(defaultCommodity.getValue());
+        mProfile.setShowCommodityByDefault(showCommodityByDefault.getValue());
+        mProfile.setPreferredAccountsFilter(preferredAccountsFilter.getValue());
+        mProfile.setUseAuthentication(useAuthentication.getValue());
+        mProfile.setAuthUser(authUserName.getValue());
+        mProfile.setAuthPassword(authPassword.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.interrupt();
+
+        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;
+        }
+        private HledgerVersion detectVersion() {
+            App.setAuthenticationDataFromProfileModel(model);
+            HttpURLConnection http;
+            try {
+                http = NetworkUtil.prepareConnection(model.getUrl(), "version",
+                        model.getUseAuthentication());
+                switch (http.getResponseCode()) {
+                    case 200:
+                        break;
+                    case 404:
+                        return new HledgerVersion(true);
+                    default:
+                        Logger.debug("profile", String.format(Locale.US,
+                                "HTTP error detecting hledger-web version: [%d] %s",
+                                http.getResponseCode(), http.getResponseMessage()));
+                        return null;
+                }
+                InputStream stream = http.getInputStream();
+                BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
+                String version = reader.readLine();
+                Matcher m = versionPattern.matcher(version);
+                if (m.matches()) {
+                    int major = Integer.parseInt(Objects.requireNonNull(m.group(1)));
+                    int minor = Integer.parseInt(Objects.requireNonNull(m.group(2)));
+                    final String patchText = m.group(3);
+                    final boolean hasPatch = patchText != null;
+                    int patch = hasPatch ? Integer.parseInt(patchText) : 0;
+
+                    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);
+            }
+        }
+    }
+}
index 6f533b01d3abffab5ef4d0a2e1afec955a278bea..08a071ddf63106703fbd10c00c501c6065b22599 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -17,8 +17,6 @@
 
 package net.ktnx.mobileledger.ui.profiles;
 
 
 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.graphics.drawable.ColorDrawable;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -30,40 +28,48 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import android.widget.TextView;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.lifecycle.MutableLiveData;
 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 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.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
-import net.ktnx.mobileledger.ui.activity.ProfileDetailActivity;
 import net.ktnx.mobileledger.utils.Colors;
 
 import net.ktnx.mobileledger.utils.Colors;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Locale;
+import java.util.List;
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class ProfilesRecyclerViewAdapter
         extends RecyclerView.Adapter<ProfilesRecyclerViewAdapter.ProfileListViewHolder> {
 
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class ProfilesRecyclerViewAdapter
         extends RecyclerView.Adapter<ProfilesRecyclerViewAdapter.ProfileListViewHolder> {
-    private static WeakReference<ProfilesRecyclerViewAdapter> instanceRef;
-    private final View.OnClickListener mOnClickListener = view -> {
-        MobileLedgerProfile profile = (MobileLedgerProfile) ((View) view.getParent()).getTag();
-        editProfile(view, profile);
-    };
-    public MutableLiveData<Boolean> editingProfiles = new MutableLiveData<>(false);
+    public final MutableLiveData<Boolean> editingProfiles = new MutableLiveData<>(false);
+    private final ItemTouchHelper rearrangeHelper;
+    private final AsyncListDiffer<Profile> listDiffer;
     private RecyclerView recyclerView;
     private RecyclerView recyclerView;
-    private ItemTouchHelper rearrangeHelper;
     private boolean animationsEnabled = true;
     private boolean animationsEnabled = true;
+
     public ProfilesRecyclerViewAdapter() {
     public ProfilesRecyclerViewAdapter() {
-        instanceRef = new WeakReference<>(this);
         debug("flow", "ProfilesRecyclerViewAdapter.new()");
 
         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,
         ItemTouchHelper.Callback cb = new ItemTouchHelper.Callback() {
             @Override
             public int getMovementFlags(@NonNull RecyclerView recyclerView,
@@ -74,12 +80,14 @@ public class ProfilesRecyclerViewAdapter
             public boolean onMove(@NonNull RecyclerView recyclerView,
                                   @NonNull RecyclerView.ViewHolder viewHolder,
                                   @NonNull RecyclerView.ViewHolder target) {
             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
                 return true;
             }
             @Override
@@ -88,9 +96,14 @@ public class ProfilesRecyclerViewAdapter
         };
         rearrangeHelper = new ItemTouchHelper(cb);
     }
         };
         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;
     }
     public void setAnimationsEnabled(boolean animationsEnabled) {
         this.animationsEnabled = animationsEnabled;
@@ -105,101 +118,75 @@ public class ProfilesRecyclerViewAdapter
     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
         super.onAttachedToRecyclerView(recyclerView);
         this.recyclerView = recyclerView;
     public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
         super.onAttachedToRecyclerView(recyclerView);
         this.recyclerView = recyclerView;
-        if (editingProfiles.getValue()) rearrangeHelper.attachToRecyclerView(recyclerView);
+        if (editingProfiles())
+            rearrangeHelper.attachToRecyclerView(recyclerView);
+    }
+    public boolean editingProfiles() {
+        final Boolean b = editingProfiles.getValue();
+        if (b == null)
+            return false;
+        return b;
     }
     public void startEditingProfiles() {
     }
     public void startEditingProfiles() {
-        if (editingProfiles.getValue()) return;
+        if (editingProfiles())
+            return;
         this.editingProfiles.setValue(true);
         rearrangeHelper.attachToRecyclerView(recyclerView);
     }
     public void stopEditingProfiles() {
         this.editingProfiles.setValue(true);
         rearrangeHelper.attachToRecyclerView(recyclerView);
     }
     public void stopEditingProfiles() {
-        if (!editingProfiles.getValue()) return;
+        if (!editingProfiles())
+            return;
         this.editingProfiles.setValue(false);
         rearrangeHelper.attachToRecyclerView(null);
     }
     public void flipEditingProfiles() {
         this.editingProfiles.setValue(false);
         rearrangeHelper.attachToRecyclerView(null);
     }
     public void flipEditingProfiles() {
-        if (editingProfiles.getValue()) stopEditingProfiles();
-        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.getValue()) return;
-        MobileLedgerProfile profile = (MobileLedgerProfile) v.getTag();
-        if (profile == null)
-            throw new IllegalStateException("Profile row without associated profile");
-        debug("profiles", "Setting profile to " + profile.getName());
-        Data.setCurrentProfile(profile);
+        if (editingProfiles())
+            stopEditingProfiles();
+        else
+            startEditingProfiles();
     }
     @NonNull
     @Override
     public ProfileListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
         View view = LayoutInflater.from(parent.getContext())
     }
     @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.getValue()) {
-                rearrangeHelper.startDrag(holder);
-                return true;
-            }
-            return false;
-        };
-
-        holder.tagAndHandleLayout.setOnTouchListener(dragStarter);
-        return holder;
+                                  .inflate(R.layout.profile_list_content, parent, false);
+        return new ProfileListViewHolder(view);
     }
     @Override
     public void onBindViewHolder(@NonNull final ProfileListViewHolder holder, int position) {
     }
     @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.profile.getValue();
-        debug("profiles", String.format(Locale.ENGLISH, "pos %d: %s, current: %s", position,
-                profile.getUuid(), (currentProfile == null) ? "<NULL>" : currentProfile.getUuid()));
-        holder.itemView.setTag(profile);
-
-        int hue = profile.getThemeId();
-        if (hue == -1) holder.mColorTag
-                .setBackgroundColor(Colors.getPrimaryColorForHue(Colors.DEFAULT_HUE_DEG));
-        else holder.mColorTag.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
+        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()));
+
+        int hue = profile.getTheme();
+        if (hue == -1)
+            holder.mColorTag.setBackgroundColor(
+                    Colors.getPrimaryColorForHue(Colors.DEFAULT_HUE_DEG));
+        else
+            holder.mColorTag.setBackgroundColor(Colors.getPrimaryColorForHue(hue));
 
         holder.mTitle.setText(profile.getName());
 //            holder.mSubTitle.setText(profile.getUrl());
 
 
         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 != null) && currentProfile.equals(profile);
-        holder.itemView
-                .setBackground(sameProfile ? new ColorDrawable(Colors.tableRowDarkBG) : null);
-        if (editingProfiles.getValue()) {
+        final boolean sameProfile =
+                currentProfile != null && currentProfile.getId() == profile.getId();
+        holder.itemView.setBackground(
+                sameProfile ? new ColorDrawable(Colors.tableRowDarkBG) : null);
+        if (editingProfiles()) {
             boolean wasHidden = holder.mEditButton.getVisibility() == View.GONE;
             holder.mRearrangeHandle.setVisibility(View.VISIBLE);
             holder.mEditButton.setVisibility(View.VISIBLE);
             if (wasHidden && animationsEnabled) {
             boolean wasHidden = holder.mEditButton.getVisibility() == View.GONE;
             holder.mRearrangeHandle.setVisibility(View.VISIBLE);
             holder.mEditButton.setVisibility(View.VISIBLE);
             if (wasHidden && animationsEnabled) {
-                Animation a = AnimationUtils
-                        .loadAnimation(holder.mRearrangeHandle.getContext(), R.anim.fade_in);
+                Animation a = AnimationUtils.loadAnimation(holder.mRearrangeHandle.getContext(),
+                        R.anim.fade_in);
                 holder.mRearrangeHandle.startAnimation(a);
                 holder.mEditButton.startAnimation(a);
             }
                 holder.mRearrangeHandle.startAnimation(a);
                 holder.mEditButton.startAnimation(a);
             }
@@ -209,8 +196,8 @@ public class ProfilesRecyclerViewAdapter
             holder.mRearrangeHandle.setVisibility(View.INVISIBLE);
             holder.mEditButton.setVisibility(View.GONE);
             if (wasShown && animationsEnabled) {
             holder.mRearrangeHandle.setVisibility(View.INVISIBLE);
             holder.mEditButton.setVisibility(View.GONE);
             if (wasShown && animationsEnabled) {
-                Animation a = AnimationUtils
-                        .loadAnimation(holder.mRearrangeHandle.getContext(), R.anim.fade_out);
+                Animation a = AnimationUtils.loadAnimation(holder.mRearrangeHandle.getContext(),
+                        R.anim.fade_out);
                 holder.mRearrangeHandle.startAnimation(a);
                 holder.mEditButton.startAnimation(a);
             }
                 holder.mRearrangeHandle.startAnimation(a);
                 holder.mEditButton.startAnimation(a);
             }
@@ -218,8 +205,8 @@ public class ProfilesRecyclerViewAdapter
     }
     @Override
     public int getItemCount() {
     }
     @Override
     public int getItemCount() {
-        final ArrayList<MobileLedgerProfile> profiles = Data.profiles.getValue();
-        return profiles != null ? profiles.size() : 0;
+        return listDiffer.getCurrentList()
+                         .size();
     }
     class ProfileListViewHolder extends RecyclerView.ViewHolder {
         final TextView mEditButton;
     }
     class ProfileListViewHolder extends RecyclerView.ViewHolder {
         final TextView mEditButton;
@@ -236,6 +223,47 @@ public class ProfilesRecyclerViewAdapter
             mRearrangeHandle = view.findViewById(R.id.profile_list_rearrange_handle);
             tagAndHandleLayout = view.findViewById(R.id.handle_and_tag);
             mRow = (ConstraintLayout) view;
             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());
         }
         }
+
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsAdapter.java
new file mode 100644 (file)
index 0000000..ccf1b81
--- /dev/null
@@ -0,0 +1,940 @@
+/*
+ * 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");
+        }
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsFragment.java
new file mode 100644 (file)
index 0000000..03dd0dd
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateDetailsViewModel.java
new file mode 100644 (file)
index 0000000..414340c
--- /dev/null
@@ -0,0 +1,338 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListDivider.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListDivider.java
new file mode 100644 (file)
index 0000000..f841ad7
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateListFragment.java
new file mode 100644 (file)
index 0000000..53eb604
--- /dev/null
@@ -0,0 +1,162 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplateViewHolder.java
new file mode 100644 (file)
index 0000000..aeb1dc1
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesActivity.java
new file mode 100644 (file)
index 0000000..3f9535c
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * 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
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java b/app/src/main/java/net/ktnx/mobileledger/ui/templates/TemplatesRecyclerViewAdapter.java
new file mode 100644 (file)
index 0000000..0b1c827
--- /dev/null
@@ -0,0 +1,143 @@
+/*
+ * 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;
+        }
+    }
+}
index 13983309e3830f2aeeaedf135b90c868a2d89ba1..bc463ed564661bb6d3da11155158322bbb2b5b29 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.ui.transaction_list;
 
-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.Gravity;
 import android.view.LayoutInflater;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 
 import androidx.annotation.NonNull;
-import androidx.appcompat.widget.AppCompatTextView;
+import androidx.recyclerview.widget.AsyncListDiffer;
+import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.RecyclerView;
 
 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.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.Colors;
-import net.ktnx.mobileledger.utils.Globals;
-
-import java.text.DateFormat;
-import java.util.Date;
-import java.util.GregorianCalendar;
-import java.util.TimeZone;
-
-import static net.ktnx.mobileledger.utils.DimensionUtils.dp2px;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.Misc;
+
+import java.util.List;
+import java.util.Locale;
+
+public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolderBase> {
+    private final AsyncListDiffer<TransactionListItem> listDiffer;
+    public TransactionListAdapter() {
+        super();
+
+        setHasStableIds(true);
+
+        listDiffer = new AsyncListDiffer<>(this, new DiffUtil.ItemCallback<TransactionListItem>() {
+            @Override
+            public boolean areItemsTheSame(@NonNull TransactionListItem oldItem,
+                                           @NonNull TransactionListItem newItem) {
+                if (oldItem.getType() != newItem.getType())
+                    return false;
+                switch (oldItem.getType()) {
+                    case DELIMITER:
+                        return (oldItem.getDate()
+                                       .equals(newItem.getDate()));
+                    case TRANSACTION:
+                        return oldItem.getTransaction()
+                                      .getLedgerId() == newItem.getTransaction()
+                                                               .getLedgerId();
+                    case HEADER:
+                        return true;    // there can be only one header
+                    default:
+                        throw new IllegalStateException(
+                                String.format(Locale.US, "Unexpected transaction item type %s",
+                                        oldItem.getType()));
+                }
+            }
+            @Override
+            public boolean areContentsTheSame(@NonNull TransactionListItem oldItem,
+                                              @NonNull TransactionListItem newItem) {
+                switch (oldItem.getType()) {
+                    case DELIMITER:
+                        return oldItem.isMonthShown() == newItem.isMonthShown();
+                    case TRANSACTION:
+                        return oldItem.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
+                        return true;
+                    default:
+                        throw new IllegalStateException(
+                                String.format(Locale.US, "Unexpected transaction item type %s",
+                                        oldItem.getType()));
 
 
-public class TransactionListAdapter extends RecyclerView.Adapter<TransactionRowHolder> {
-    public void onBindViewHolder(@NonNull TransactionRowHolder holder, int position) {
-        TransactionListItem item = TransactionListViewModel.getTransactionListItem(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);
 
         // in a race when transaction value is reduced, but the model hasn't been notified yet
         // the view will disappear when the notifications reaches the model, so by simply omitting
         // the out-of-range get() call nothing bad happens - just a to-be-deleted view remains
         // a bit longer
 
         // in a race when transaction value is reduced, but the model hasn't been notified yet
         // the view will disappear when the notifications reaches the model, so by simply omitting
         // the out-of-range get() call nothing bad happens - just a to-be-deleted view remains
         // a bit longer
-        if (item == null) return;
+        if (item == null)
+            return;
 
 
-        switch (item.getType()) {
+        final TransactionListItem.Type newType = item.getType();
+
+        switch (newType) {
             case TRANSACTION:
             case TRANSACTION:
-                holder.vTransaction.setVisibility(View.VISIBLE);
-                holder.vDelimiter.setVisibility(View.GONE);
-                LedgerTransaction tr = item.getTransaction();
-
-                //        debug("transactions", String.format("Filling position %d with %d accounts", position,
-                //                tr.getAccounts().size()));
-
-                TransactionLoader loader = new TransactionLoader();
-                loader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
-                        new TransactionLoaderParams(tr, holder, position, Data.accountFilter.getValue(),
-                                item.isOdd()));
-
-                // 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));
+                holder.asTransaction()
+                      .bind(item, item.getBoldAccountName());
+
                 break;
             case DELIMITER:
                 break;
             case DELIMITER:
-                Date date = item.getDate();
-                holder.vTransaction.setVisibility(View.GONE);
-                holder.vDelimiter.setVisibility(View.VISIBLE);
-                holder.tvDelimiterDate.setText(DateFormat.getDateInstance().format(date));
-                if (item.isMonthShown()) {
-                    GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
-                    cal.setTime(date);
-                    holder.tvDelimiterMonth
-                            .setText(Globals.monthNames[cal.get(GregorianCalendar.MONTH)]);
-                    holder.tvDelimiterMonth.setVisibility(View.VISIBLE);
-                    //                holder.vDelimiterLine.setBackgroundResource(R.drawable.dashed_border_8dp);
-                    holder.vDelimiterLine.setVisibility(View.GONE);
-                    holder.vDelimiterThick.setVisibility(View.VISIBLE);
-                }
-                else {
-                    holder.tvDelimiterMonth.setVisibility(View.GONE);
-                    //                holder.vDelimiterLine.setBackgroundResource(R.drawable.dashed_border_1dp);
-                    holder.vDelimiterLine.setVisibility(View.VISIBLE);
-                    holder.vDelimiterThick.setVisibility(View.GONE);
-                }
+                holder.asDelimiter()
+                      .bind(item);
                 break;
                 break;
+            case HEADER:
+                holder.asHeader()
+                      .bind();
+
+                break;
+            default:
+                throw new IllegalStateException("Unexpected value: " + newType);
         }
     }
         }
     }
-
     @NonNull
     @Override
     @NonNull
     @Override
-    public TransactionRowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+    public TransactionRowHolderBase onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
 //        debug("perf", "onCreateViewHolder called");
 //        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
     public int getItemCount() {
     }
 
     @Override
     public int getItemCount() {
-        return Data.transactions.size();
+        return listDiffer.getCurrentList()
+                         .size();
     }
     }
-    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;
-            boolean odd = p[0].odd;
-
-            SQLiteDatabase db = App.getDatabase();
-            tr.loadData(db);
-
-            publishProgress(new TransactionLoaderStep(p[0].holder, p[0].position, tr, odd));
-
-            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());
-
-                    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);
-                    TextView accName, accAmount;
-                    if (row == null) {
-                        row = new LinearLayout(ctx);
-                        row.setLayoutParams(new LinearLayout.LayoutParams(
-                                LinearLayout.LayoutParams.MATCH_PARENT,
-                                LinearLayout.LayoutParams.WRAP_CONTENT));
-                        row.setGravity(Gravity.CENTER_VERTICAL);
-                        row.setOrientation(LinearLayout.HORIZONTAL);
-                        row.setPaddingRelative(dp2px(ctx, 8), 0, 0, 0);
-                        accName = new AppCompatTextView(ctx);
-                        accName.setLayoutParams(new LinearLayout.LayoutParams(0,
-                                LinearLayout.LayoutParams.WRAP_CONTENT, 5f));
-                        accName.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
-                        row.addView(accName);
-                        accAmount = new AppCompatTextView(ctx);
-                        LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(
-                                LinearLayout.LayoutParams.WRAP_CONTENT,
-                                LinearLayout.LayoutParams.WRAP_CONTENT);
-                        llp.setMarginEnd(0);
-                        accAmount.setLayoutParams(llp);
-                        accAmount.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
-                        accAmount.setMinWidth(dp2px(ctx, 60));
-                        row.addView(accAmount);
-                        holder.tableAccounts.addView(row);
-                    }
-                    else {
-                        accName = (TextView) row.getChildAt(0);
-                        accAmount = (TextView) row.getChildAt(1);
-                    }
-                    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.accent);
-                        accAmount.setTextColor(Colors.accent);
-
-                        SpannableString ss = new SpannableString(acc.getAccountName());
-                        ss.setSpan(new StyleSpan(Typeface.BOLD), 0, boldAccountName.length(),
-                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-                        accName.setText(ss);
-                    }
-                    else {
-                        accName.setTextColor(Colors.defaultTextColor);
-                        accAmount.setTextColor(Colors.defaultTextColor);
-                        accName.setText(acc.getAccountName());
-                    }
-                    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 class TransactionLoaderParams {
-        LedgerTransaction transaction;
-        TransactionRowHolder holder;
-        int position;
-        String boldAccountName;
-        boolean odd;
-        TransactionLoaderParams(LedgerTransaction transaction, TransactionRowHolder holder,
-                                int position, String boldAccountName, boolean odd) {
-            this.transaction = transaction;
-            this.holder = holder;
-            this.position = position;
-            this.boldAccountName = boldAccountName;
-            this.odd = odd;
-        }
+    public void setTransactions(List<TransactionListItem> newList) {
+        Logger.debug("transactions",
+                String.format(Locale.US, "Got new transaction list (%d items)", newList.size()));
+        listDiffer.submitList(newList);
     }
 }
\ No newline at end of file
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListDelimiterRowHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListDelimiterRowHolder.java
new file mode 100644 (file)
index 0000000..b5fe883
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * 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);
+        }
+
+    }
+}
index 8de45dce7712ceacb440240d31b068f118a21cc4..a31b657fdc52b106e6ffd327e040b0d9cc3118e1 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -17,8 +17,6 @@
 
 package net.ktnx.mobileledger.ui.transaction_list;
 
 
 package net.ktnx.mobileledger.ui.transaction_list;
 
-import android.content.Context;
-import android.database.MatrixCursor;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -27,164 +25,212 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.inputmethod.InputMethodManager;
-import android.widget.AutoCompleteTextView;
-import android.widget.Toast;
+
+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.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.Data;
+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.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.MLDB;
+import net.ktnx.mobileledger.utils.Logger;
+import net.ktnx.mobileledger.utils.SimpleDate;
 
 import org.jetbrains.annotations.NotNull;
 
 
 import org.jetbrains.annotations.NotNull;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
+import java.util.Locale;
 
 import static android.content.Context.INPUT_METHOD_SERVICE;
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 
 import static android.content.Context.INPUT_METHOD_SERVICE;
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
-public class TransactionListFragment extends MobileLedgerListFragment {
+public class TransactionListFragment extends MobileLedgerListFragment
+        implements DatePickerFragment.DatePickedListener {
     private MenuItem menuTransactionListFilter;
     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);
         setHasOptionsMenu(true);
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setHasOptionsMenu(true);
-        Data.backgroundTasksRunning.observe(this, this::onBackgroundTaskRunningChanged);
-    }
-    @Override
-    public void onAttach(@NotNull Context context) {
-        super.onAttach(context);
-        mActivity = (MainActivity) context;
     }
     @Nullable
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                              @Nullable Bundle savedInstanceState) {
     }
     @Nullable
     @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();
     }
     @Override
     public void onResume() {
         super.onResume();
+        fragmentActive = true;
+        toggleMenuItems();
         debug("flow", "TransactionListFragment.onResume()");
     }
         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();
     @Override
     public void onStop() {
         super.onStop();
+        fragmentActive = false;
+        toggleMenuItems();
         debug("flow", "TransactionListFragment.onStop()");
     }
     @Override
     public void onPause() {
         super.onPause();
         debug("flow", "TransactionListFragment.onStop()");
     }
     @Override
     public void onPause() {
         super.onPause();
+        fragmentActive = false;
+        toggleMenuItems();
         debug("flow", "TransactionListFragment.onPause()");
     }
     @Override
         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");
         debug("flow", "TransactionListFragment.onActivityCreated called");
-        super.onActivityCreated(savedInstanceState);
+        super.onViewCreated(view, savedInstanceState);
+        Data.backgroundTasksRunning.observe(getViewLifecycleOwner(),
+                this::onBackgroundTaskRunningChanged);
+
+        MainActivity mainActivity = getMainActivity();
+
+        model = new ViewModelProvider(requireActivity()).get(MainModel.class);
 
 
-        swiper = mActivity.findViewById(R.id.transaction_swipe);
-        if (swiper == null) throw new RuntimeException("Can't get hold on the swipe layout");
-        root = mActivity.findViewById(R.id.transaction_root);
-        if (root == null)
-            throw new RuntimeException("Can't get hold on the transaction value view");
         modelAdapter = new TransactionListAdapter();
         modelAdapter = new TransactionListAdapter();
-        root.setAdapter(modelAdapter);
+        b.transactionRoot.setAdapter(modelAdapter);
 
 
-        mActivity.fabShouldShow();
+        mainActivity.fabShouldShow();
 
 
-        manageFabOnScroll();
+        if (mainActivity instanceof FabManager.FabHandler)
+            FabManager.handle(mainActivity, b.transactionRoot);
 
 
-        LinearLayoutManager llm = new LinearLayoutManager(mActivity);
+        LinearLayoutManager llm = new LinearLayoutManager(mainActivity);
 
         llm.setOrientation(RecyclerView.VERTICAL);
 
         llm.setOrientation(RecyclerView.VERTICAL);
-        root.setLayoutManager(llm);
+        b.transactionRoot.setLayoutManager(llm);
 
 
-        swiper.setOnRefreshListener(() -> {
+        b.transactionSwipe.setOnRefreshListener(() -> {
             debug("ui", "refreshing transactions via swipe");
             debug("ui", "refreshing transactions via swipe");
-            Data.scheduleTransactionListRetrieval(mActivity);
+            model.scheduleTransactionListRetrieval();
         });
 
         });
 
-        Colors.themeWatch.observe(this, this::themeChanged);
+        Colors.themeWatch.observe(getViewLifecycleOwner(), this::themeChanged);
 
 
-        vAccountFilter = mActivity.findViewById(R.id.transaction_list_account_name_filter);
-        accNameFilter = mActivity.findViewById(R.id.transaction_filter_account_name);
+        Data.observeProfile(getViewLifecycleOwner(), this::onProfileChanged);
 
 
-        MLDB.hookAutocompletionAdapter(mActivity, accNameFilter, "accounts", "name");
-        accNameFilter.setOnItemClickListener((parent, view, position, id) -> {
+        b.transactionFilterAccountName.setOnItemClickListener((parent, v, position, id) -> {
 //                debug("tmp", "direct onItemClick");
 //                debug("tmp", "direct onItemClick");
-            MatrixCursor mc = (MatrixCursor) parent.getItemAtPosition(position);
-            Data.accountFilter.setValue(mc.getString(1));
-            Globals.hideSoftKeyboard(mActivity);
+            model.getAccountFilter()
+                 .setValue(parent.getItemAtPosition(position)
+                                 .toString());
+            Globals.hideSoftKeyboard(mainActivity);
         });
 
         });
 
-        Data.accountFilter.observe(this, this::onAccountNameFilterChanged);
-
-        TransactionListViewModel.updating.addObserver(
-                (o, arg) -> swiper.setRefreshing(TransactionListViewModel.updating.get()));
-        TransactionListViewModel.updateError.addObserver((o, arg) -> {
-            String err = TransactionListViewModel.updateError.get();
-            if (err == null) return;
-
-            Toast.makeText(mActivity, err, Toast.LENGTH_SHORT).show();
-            TransactionListViewModel.updateError.set(null);
-        });
-        Data.transactions.addObserver(
-                (o, arg) -> mActivity.runOnUiThread(() -> modelAdapter.notifyDataSetChanged()));
-
-        mActivity.findViewById(R.id.clearAccountNameFilter).setOnClickListener(v -> {
-            Data.accountFilter.setValue(null);
-            vAccountFilter.setVisibility(View.GONE);
-            menuTransactionListFilter.setVisible(true);
-            Globals.hideSoftKeyboard(mActivity);
+        model.getAccountFilter()
+             .observe(getViewLifecycleOwner(), this::onAccountNameFilterChanged);
+
+        model.getUpdatingFlag()
+             .observe(getViewLifecycleOwner(), (flag) -> b.transactionSwipe.setRefreshing(flag));
+        model.getDisplayedTransactions()
+             .observe(getViewLifecycleOwner(), list -> modelAdapter.setTransactions(list));
+
+        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) {
+                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(""))) return;
+    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);
-        }
-        if (menuTransactionListFilter != null) menuTransactionListFilter.setVisible(!filterActive);
-
-        TransactionListViewModel.scheduleTransactionListReload();
+        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);
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
         inflater.inflate(R.menu.transaction_list, menu);
 
         menuTransactionListFilter = menu.findItem(R.id.menu_transaction_list_filter);
     }
     @Override
     public void onCreateOptionsMenu(@NotNull Menu menu, @NotNull MenuInflater inflater) {
         inflater.inflate(R.menu.transaction_list, menu);
 
         menuTransactionListFilter = menu.findItem(R.id.menu_transaction_list_filter);
-        if ((menuTransactionListFilter == null)) throw new AssertionError();
+        if ((menuTransactionListFilter == null))
+            throw new AssertionError();
+        menuGoToDate = menu.findItem(R.id.menu_go_to_date);
+        if ((menuGoToDate == null))
+            throw new AssertionError();
 
 
-        if ((Data.accountFilter.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 -> {
 
         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 imm =
-                    (InputMethodManager) mActivity.getSystemService(INPUT_METHOD_SERVICE);
-            imm.showSoftInput(accNameFilter, 0);
+                    (InputMethodManager) getMainActivity().getSystemService(INPUT_METHOD_SERVICE);
+            imm.showSoftInput(b.transactionFilterAccountName, 0);
+
+            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;
         });
             return true;
         });
+
+        toggleMenuItems();
+    }
+    @Override
+    public void onDatePicked(int year, int month, int day) {
+        RecyclerView list = requireActivity().findViewById(R.id.transaction_root);
+        TransactionDateFinder finder = new TransactionDateFinder(model, new SimpleDate(year, month + 1, day));
+
+        finder.start();
     }
     }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListLastUpdateRowHolder.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListLastUpdateRowHolder.java
new file mode 100644 (file)
index 0000000..23382cc
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * 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);
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionListViewModel.java
deleted file mode 100644 (file)
index ef93d64..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.transaction_list;
-
-import android.os.AsyncTask;
-
-import net.ktnx.mobileledger.async.UpdateTransactionsTask;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.TransactionListItem;
-import net.ktnx.mobileledger.utils.LockHolder;
-import net.ktnx.mobileledger.utils.ObservableValue;
-
-import androidx.lifecycle.ViewModel;
-
-public class TransactionListViewModel extends ViewModel {
-    public static ObservableValue<Boolean> updating = new ObservableValue<>();
-    public static ObservableValue<String> updateError = new ObservableValue<>();
-
-    public static void scheduleTransactionListReload() {
-        if (Data.profile.getValue() == null) return;
-
-        String filter = Data.accountFilter.getValue();
-        AsyncTask<String, Void, String> task = new UTT();
-        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, filter);
-    }
-    public static TransactionListItem getTransactionListItem(int position) {
-        if (Data.transactions == null) return null;
-        try(LockHolder lh = Data.transactions.lockForReading()) {
-            if (position >= Data.transactions.size()) return null;
-            return Data.transactions.get(position);
-        }
-    }
-    private static class UTT extends UpdateTransactionsTask {
-        @Override
-        protected void onPostExecute(String error) {
-            super.onPostExecute(error);
-            if (error != null) updateError.set(error);
-        }
-    }
-}
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionLoaderStep.java
deleted file mode 100644 (file)
index 1dd8604..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * 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.transaction_list;
-
-import net.ktnx.mobileledger.model.LedgerTransaction;
-import net.ktnx.mobileledger.model.LedgerTransactionAccount;
-
-class TransactionLoaderStep {
-    private int position;
-    private int accountCount;
-    private TransactionListAdapter.LoaderStep step;
-    private TransactionRowHolder holder;
-    private LedgerTransaction transaction;
-    private LedgerTransactionAccount account;
-    private int accountPosition;
-    private String boldAccountName;
-    private boolean odd;
-    public TransactionLoaderStep(TransactionRowHolder holder, int position,
-                                 LedgerTransaction transaction, boolean isOdd) {
-        this.step = TransactionListAdapter.LoaderStep.HEAD;
-        this.holder = holder;
-        this.transaction = transaction;
-        this.position = position;
-        this.odd = isOdd;
-    }
-    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;
-    }
-    public boolean isOdd() {
-        return odd;
-    }
-}
index fefabcaa3e5b037eaeb4023dc146a76079d7f5ad..876430f5de06e4c1b4b25a9061a5f9d74abfe94b 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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 android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.R;
+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 androidx.annotation.NonNull;
-import androidx.cardview.widget.CardView;
-import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.recyclerview.widget.RecyclerView;
-
-class TransactionRowHolder extends RecyclerView.ViewHolder {
-    TextView tvDescription;
-    LinearLayout tableAccounts;
-    ConstraintLayout row;
-    ConstraintLayout vDelimiter;
-    CardView vTransaction;
-    TextView tvDelimiterMonth, tvDelimiterDate;
-    View vDelimiterLine, vDelimiterThick;
-    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.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.vDelimiterLine = itemView.findViewById(R.id.transaction_delimiter_line);
-        this.vDelimiterThick = itemView.findViewById(R.id.transaction_delimiter_thick);
+import java.util.Observer;
+
+class TransactionRowHolder extends TransactionRowHolderBase {
+    private final TransactionListRowBinding b;
+    TransactionListItem.Type lastType;
+    private Observer lastUpdateObserver;
+    public TransactionRowHolder(@NonNull TransactionListRowBinding binding) {
+        super(binding.getRoot());
+        b = binding;
+    }
+    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);
+        }
+
+        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);
+        }
+
+        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);
+            }
+
+            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()));
+            }
+
+            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++;
+        }
+
+        if (b.transactionRowAccAmounts.getChildCount() > rowIndex) {
+            b.transactionRowAccAmounts.removeViews(rowIndex,
+                    b.transactionRowAccAmounts.getChildCount() - rowIndex);
+        }
     }
 }
     }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolderBase.java b/app/src/main/java/net/ktnx/mobileledger/ui/transaction_list/TransactionRowHolderBase.java
new file mode 100644 (file)
index 0000000..951cab6
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * 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;
+    }
+}
index 277de9ff0c99c172c13c7262c78479fdfafaae4e..348a5598a324073ea2415f353cd32a1019b72542 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 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 android.util.TypedValue;
 
 import androidx.annotation.ColorInt;
 import android.app.Activity;
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.util.TypedValue;
 
 import androidx.annotation.ColorInt;
-import androidx.annotation.ColorLong;
 import androidx.annotation.NonNull;
 import androidx.lifecycle.MutableLiveData;
 
 import androidx.annotation.NonNull;
 import androidx.lifecycle.MutableLiveData;
 
+import net.ktnx.mobileledger.BuildConfig;
 import net.ktnx.mobileledger.R;
 import net.ktnx.mobileledger.R;
-import net.ktnx.mobileledger.model.Data;
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import net.ktnx.mobileledger.db.Profile;
 import net.ktnx.mobileledger.ui.HueRing;
 
 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.Locale;
-
-import static java.lang.Math.abs;
-import static net.ktnx.mobileledger.utils.Logger.debug;
+import java.util.Objects;
 
 public class Colors {
     public static final int DEFAULT_HUE_DEG = 261;
 
 public class Colors {
     public static final int DEFAULT_HUE_DEG = 261;
-    private static final float blueLightness = 0.665f;
-    private static final float yellowLightness = 0.350f;
+    public static final MutableLiveData<Integer> themeWatch = new MutableLiveData<>(0);
     private static final int[][] EMPTY_STATES = new int[][]{new int[0]};
     private static final int[][] EMPTY_STATES = new int[][]{new int[0]};
+    private static final int SWIPE_COLOR_COUNT = 6;
+    private static final int[] themeIDs =
+            {R.style.AppTheme_default, R.style.AppTheme_000, R.style.AppTheme_005,
+             R.style.AppTheme_010, R.style.AppTheme_015, R.style.AppTheme_020, R.style.AppTheme_025,
+             R.style.AppTheme_030, R.style.AppTheme_035, R.style.AppTheme_040, R.style.AppTheme_045,
+             R.style.AppTheme_050, R.style.AppTheme_055, R.style.AppTheme_060, R.style.AppTheme_065,
+             R.style.AppTheme_070, R.style.AppTheme_075, R.style.AppTheme_080, R.style.AppTheme_085,
+             R.style.AppTheme_090, R.style.AppTheme_095, R.style.AppTheme_100, R.style.AppTheme_105,
+             R.style.AppTheme_110, R.style.AppTheme_115, R.style.AppTheme_120, R.style.AppTheme_125,
+             R.style.AppTheme_130, R.style.AppTheme_135, R.style.AppTheme_140, R.style.AppTheme_145,
+             R.style.AppTheme_150, R.style.AppTheme_155, R.style.AppTheme_160, R.style.AppTheme_165,
+             R.style.AppTheme_170, R.style.AppTheme_175, R.style.AppTheme_180, R.style.AppTheme_185,
+             R.style.AppTheme_190, R.style.AppTheme_195, R.style.AppTheme_200, R.style.AppTheme_205,
+             R.style.AppTheme_210, R.style.AppTheme_215, R.style.AppTheme_220, R.style.AppTheme_225,
+             R.style.AppTheme_230, R.style.AppTheme_235, R.style.AppTheme_240, R.style.AppTheme_245,
+             R.style.AppTheme_250, R.style.AppTheme_255, R.style.AppTheme_260, R.style.AppTheme_265,
+             R.style.AppTheme_270, R.style.AppTheme_275, R.style.AppTheme_280, R.style.AppTheme_285,
+             R.style.AppTheme_290, R.style.AppTheme_295, R.style.AppTheme_300, R.style.AppTheme_305,
+             R.style.AppTheme_310, R.style.AppTheme_315, R.style.AppTheme_320, R.style.AppTheme_325,
+             R.style.AppTheme_330, R.style.AppTheme_335, R.style.AppTheme_340, R.style.AppTheme_345,
+             R.style.AppTheme_350, R.style.AppTheme_355,
+             };
+    private static final HashMap<Integer, Integer> themePrimaryColor = new HashMap<>();
     public static @ColorInt
     public static @ColorInt
-    int accent;
-    @ColorInt
-    public static int tableRowLightBG;
+    int primary;
     @ColorInt
     public static int tableRowDarkBG;
     @ColorInt
     public static int tableRowDarkBG;
-    @ColorInt
-    public static int primary, defaultTextColor;
-    public static int profileThemeId = -1;
-    public static MutableLiveData<Integer> themeWatch = new MutableLiveData<>(0);
-    private static int[] themeIDs =
-            {R.style.AppTheme_NoActionBar_000, R.style.AppTheme_NoActionBar_005,
-             R.style.AppTheme_NoActionBar_010, R.style.AppTheme_NoActionBar_015,
-             R.style.AppTheme_NoActionBar_020, R.style.AppTheme_NoActionBar_025,
-             R.style.AppTheme_NoActionBar_030, R.style.AppTheme_NoActionBar_035,
-             R.style.AppTheme_NoActionBar_040, R.style.AppTheme_NoActionBar_045,
-             R.style.AppTheme_NoActionBar_050, R.style.AppTheme_NoActionBar_055,
-             R.style.AppTheme_NoActionBar_060, R.style.AppTheme_NoActionBar_065,
-             R.style.AppTheme_NoActionBar_070, R.style.AppTheme_NoActionBar_075,
-             R.style.AppTheme_NoActionBar_080, R.style.AppTheme_NoActionBar_085,
-             R.style.AppTheme_NoActionBar_090, R.style.AppTheme_NoActionBar_095,
-             R.style.AppTheme_NoActionBar_100, R.style.AppTheme_NoActionBar_105,
-             R.style.AppTheme_NoActionBar_110, R.style.AppTheme_NoActionBar_115,
-             R.style.AppTheme_NoActionBar_120, R.style.AppTheme_NoActionBar_125,
-             R.style.AppTheme_NoActionBar_130, R.style.AppTheme_NoActionBar_135,
-             R.style.AppTheme_NoActionBar_140, R.style.AppTheme_NoActionBar_145,
-             R.style.AppTheme_NoActionBar_150, R.style.AppTheme_NoActionBar_155,
-             R.style.AppTheme_NoActionBar_160, R.style.AppTheme_NoActionBar_165,
-             R.style.AppTheme_NoActionBar_170, R.style.AppTheme_NoActionBar_175,
-             R.style.AppTheme_NoActionBar_180, R.style.AppTheme_NoActionBar_185,
-             R.style.AppTheme_NoActionBar_190, R.style.AppTheme_NoActionBar_195,
-             R.style.AppTheme_NoActionBar_200, R.style.AppTheme_NoActionBar_205,
-             R.style.AppTheme_NoActionBar_210, R.style.AppTheme_NoActionBar_215,
-             R.style.AppTheme_NoActionBar_220, R.style.AppTheme_NoActionBar_225,
-             R.style.AppTheme_NoActionBar_230, R.style.AppTheme_NoActionBar_235,
-             R.style.AppTheme_NoActionBar_240, R.style.AppTheme_NoActionBar_245,
-             R.style.AppTheme_NoActionBar_250, R.style.AppTheme_NoActionBar_255,
-             R.style.AppTheme_NoActionBar_260, R.style.AppTheme_NoActionBar_265,
-             R.style.AppTheme_NoActionBar_270, R.style.AppTheme_NoActionBar_275,
-             R.style.AppTheme_NoActionBar_280, R.style.AppTheme_NoActionBar_285,
-             R.style.AppTheme_NoActionBar_290, R.style.AppTheme_NoActionBar_295,
-             R.style.AppTheme_NoActionBar_300, R.style.AppTheme_NoActionBar_305,
-             R.style.AppTheme_NoActionBar_310, R.style.AppTheme_NoActionBar_315,
-             R.style.AppTheme_NoActionBar_320, R.style.AppTheme_NoActionBar_325,
-             R.style.AppTheme_NoActionBar_330, R.style.AppTheme_NoActionBar_335,
-             R.style.AppTheme_NoActionBar_340, R.style.AppTheme_NoActionBar_345,
-             R.style.AppTheme_NoActionBar_350, R.style.AppTheme_NoActionBar_355,
-             };
+    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;
     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.table_row_light_bg, tv, true);
-        tableRowLightBG = tv.data;
-        theme.resolveAttribute(R.attr.colorPrimary, tv, true);
+        theme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, tv, true);
         primary = tv.data;
         primary = tv.data;
-        theme.resolveAttribute(R.attr.textColor, tv, true);
-        defaultTextColor = tv.data;
-        theme.resolveAttribute(R.attr.colorAccent, tv, true);
-        accent = tv.data;
+
+        if (themePrimaryColor.size() == 0) {
+            for (int themeId : themeIDs) {
+                Resources.Theme tmpTheme = theme.getResources()
+                                                .newTheme();
+                tmpTheme.applyStyle(themeId, true);
+                tmpTheme.resolveAttribute(androidx.appcompat.R.attr.colorPrimary, tv, false);
+                themePrimaryColor.put(themeId, tv.data);
+            }
+        }
 
         // trigger theme observers
         themeWatch.postValue(themeWatch.getValue() + 1);
     }
 
         // trigger theme observers
         themeWatch.postValue(themeWatch.getValue() + 1);
     }
-    public static @ColorLong
-    long hsvaColor(float hue, float saturation, float value, float alpha) {
-        if (alpha < 0 || alpha > 1)
-            throw new IllegalArgumentException("alpha must be between 0 and 1");
-
-        @ColorLong long rgb = hsvTriplet(hue, saturation, value);
-
-        long a_bits = Math.round(255 * alpha);
-        return (a_bits << 24) | rgb;
-    }
     public static @ColorInt
     public static @ColorInt
-    int hsvColor(float hue, float saturation, float value) {
-        return 0xff000000 | hsvTriplet(hue, saturation, value);
-    }
-    public static @ColorInt
-    int hslColor(float hueRatio, float saturation, float lightness) {
-        return 0xff000000 | hslTriplet(hueRatio, saturation, lightness);
-    }
-    public static @ColorInt
-    int hsvTriplet(float hue, float saturation, float value) {
-        @ColorLong long result;
-        int r, g, b;
-
-        if ((hue < -0.00005) || (hue > 1.0000005) || (saturation < 0) || (saturation > 1) ||
-            (value < 0) || (value > 1)) throw new IllegalArgumentException(String.format(
-                "hue, saturation, value and alpha must all be between 0 and 1. Arguments given: " +
-                "hue=%1.5f, sat=%1.5f, val=%1.5f", hue, saturation, value));
-
-        int h = (int) (hue * 6);
-        float f = hue * 6 - h;
-        float p = value * (1 - saturation);
-        float q = value * (1 - f * saturation);
-        float t = value * (1 - (1 - f) * saturation);
-
-        switch (h) {
-            case 0:
-            case 6:
-                return tupleToColor(value, t, p);
-            case 1:
-                return tupleToColor(q, value, p);
-            case 2:
-                return tupleToColor(p, value, t);
-            case 3:
-                return tupleToColor(p, q, value);
-            case 4:
-                return tupleToColor(t, p, value);
-            case 5:
-                return tupleToColor(value, p, q);
-            default:
-                throw new RuntimeException(String.format("Unexpected value for h (%d) while " +
-                                                         "converting hsv(%1.2f, %1.2f, %1.2f) to " +
-                                                         "rgb", h, hue, saturation, value));
+    int getPrimaryColorForHue(int hueDegrees) {
+        if (hueDegrees == DEFAULT_HUE_DEG)
+            return Objects.requireNonNull(themePrimaryColor.get(R.style.AppTheme_default));
+        int mod = hueDegrees % HueRing.hueStepDegrees;
+        if (mod == 0) {
+            int themeId = getThemeIdForHue(hueDegrees);
+            Integer result = Objects.requireNonNull(themePrimaryColor.get(themeId));
+            debug("colors",
+                    String.format(Locale.US, "getPrimaryColorForHue(%d) = %x", hueDegrees, result));
+            return result;
+        }
+        else {
+            int x0 = hueDegrees - mod;
+            int x1 = (x0 + HueRing.hueStepDegrees) % 360;
+            float y0 = Objects.requireNonNull(themePrimaryColor.get(getThemeIdForHue(x0)));
+            float y1 = Objects.requireNonNull(themePrimaryColor.get(getThemeIdForHue(x1)));
+            return Math.round(y0 + hueDegrees * (y1 - y0) / (x1 - x0));
         }
     }
         }
     }
-    public static @ColorInt
-    int hslTriplet(float hueRatio, float saturation, float lightness) {
-        @ColorLong long result;
-        float h = hueRatio * 6;
-        float c = (1 - abs(2f * lightness - 1)) * saturation;
-        float h_mod_2 = h % 2;
-        float x = c * (1 - Math.abs(h_mod_2 - 1));
-        int r, g, b;
-        float m = lightness - c / 2f;
-
-        if (h < 1 || h == 6) return tupleToColor(c + m, x + m, 0 + m);
-        if (h < 2) return tupleToColor(x + m, c + m, 0 + m);
-        if (h < 3) return tupleToColor(0 + m, c + m, x + m);
-        if (h < 4) return tupleToColor(0 + m, x + m, c + m);
-        if (h < 5) return tupleToColor(x + m, 0 + m, c + m);
-        if (h < 6) return tupleToColor(c + m, 0 + m, x + m);
-
-        throw new IllegalArgumentException(String.format(
-                "Unexpected value for h (%1.3f) while converting hsl(%1.3f, %1.3f, %1.3f) to rgb",
-                h, hueRatio, saturation, lightness));
-    }
+    public static int getThemeIdForHue(int themeHue) {
+        int themeIndex = -1;
+        if (themeHue == 360)
+            themeHue = 0;
+        if ((themeHue >= 0) && (themeHue < 360) && (themeHue != DEFAULT_HUE_DEG)) {
+            if ((themeHue % HueRing.hueStepDegrees) != 0) {
+                Logger.warn("profiles",
+                        String.format(Locale.US, "Adjusting unexpected hue %d", themeHue));
+                themeIndex = Math.round(1f * themeHue / HueRing.hueStepDegrees);
+            }
+            else
+                themeIndex = themeHue / HueRing.hueStepDegrees;
+        }
 
 
-    public static @ColorInt
-    int tupleToColor(float r, float g, float b) {
-        int r_int = Math.round(255 * r);
-        int g_int = Math.round(255 * g);
-        int b_int = Math.round(255 * b);
-        return (r_int << 16) | (g_int << 8) | b_int;
-    }
-    public static @ColorInt
-    int getPrimaryColorForHue(int hueDegrees) {
-//        int result = hsvColor(hueDegrees, 0.61f, 0.95f);
-        float y = hueDegrees - 60;
-        if (y < 0) y += 360;
-        float l = yellowLightness + (blueLightness - yellowLightness) *
-                                    (float) Math.cos(Math.toRadians(Math.abs(180 - y) / 2f));
-        int result = hslColor(hueDegrees / 360f, 0.845f, l);
-        debug("colors", String.format(Locale.ENGLISH, "getPrimaryColorForHue(%d) = %x", hueDegrees,
-                result));
-        return result;
-    }
-    public static void setupTheme(Activity activity) {
-        MobileLedgerProfile profile = Data.profile.getValue();
-        setupTheme(activity, profile);
-    }
-    public static void setupTheme(Activity activity, MobileLedgerProfile profile) {
-        final int themeHue = (profile == null) ? -1 : profile.getThemeId();
-        setupTheme(activity, themeHue);
+        return themeIDs[themeIndex + 1];    // 0 is the default theme
     }
     public static void setupTheme(Activity activity, int themeHue) {
     }
     public static void setupTheme(Activity activity, int themeHue) {
-        int themeId = -1;
-        // Relies that theme resource IDs are sequential numbers
-        if (themeHue == 360) themeHue = 0;
-        if ((themeHue >= 0) && (themeHue < 360) && ((themeHue % HueRing.hueStepDegrees) == 0)) {
-            themeId = themeIDs[themeHue / HueRing.hueStepDegrees];
-        }
-
-        if (themeId < 0) {
-            activity.setTheme(R.style.AppTheme_NoActionBar);
-            debug("profiles",
-                    String.format(Locale.ENGLISH, "Theme hue %d not supported, using the default",
-                            themeHue));
-        }
-        else {
-            activity.setTheme(themeId);
-        }
+        int themeId = getThemeIdForHue(themeHue);
+        activity.setTheme(themeId);
 
         refreshColors(activity.getTheme());
     }
 
         refreshColors(activity.getTheme());
     }
-
     public static @NonNull
     ColorStateList getColorStateList() {
         return getColorStateList(profileThemeId);
     }
     public static @NonNull
     ColorStateList getColorStateList(int hue) {
     public static @NonNull
     ColorStateList getColorStateList() {
         return getColorStateList(profileThemeId);
     }
     public static @NonNull
     ColorStateList getColorStateList(int hue) {
-        return new ColorStateList(EMPTY_STATES, getColors(hue));
+        return new ColorStateList(EMPTY_STATES, getSwipeCircleColors(hue));
     }
     }
-    public static int[] getColors() {
-        return getColors(profileThemeId);
+    public static int[] getSwipeCircleColors() {
+        return getSwipeCircleColors(profileThemeId);
     }
     }
-    public static int[] getColors(int hue) {
-        int[] colors = new int[]{0, 0, 0, 0, 0, 0};
-        for (int i = 0; i < 6; i++, hue = (hue + 60) % 360) {
+    public static int[] getSwipeCircleColors(int hue) {
+        int[] colors = new int[SWIPE_COLOR_COUNT];
+        for (int i = 0; i < SWIPE_COLOR_COUNT; i++, hue = (hue + 360 / SWIPE_COLOR_COUNT) % 360) {
             colors[i] = getPrimaryColorForHue(hue);
         }
         return colors;
     }
             colors[i] = getPrimaryColorForHue(hue);
         }
         return colors;
     }
+    public static int getNewProfileThemeHue(List<Profile> profiles) {
+        if ((profiles == null) || (profiles.size() == 0))
+            return DEFAULT_HUE_DEG;
+
+        int chosenHue;
+
+        if (profiles.size() == 1) {
+            int opposite = profiles.get(0)
+                                   .getTheme() + 180;
+            opposite %= 360;
+            chosenHue = opposite;
+        }
+        else {
+            ArrayList<Integer> hues = new ArrayList<>();
+            for (Profile p : profiles) {
+                int hue = p.getTheme();
+                if (hue == -1)
+                    hue = DEFAULT_HUE_DEG;
+                hues.add(hue);
+            }
+            Collections.sort(hues);
+            if (BuildConfig.DEBUG) {
+                StringBuilder huesSB = new StringBuilder();
+                for (int h : hues) {
+                    if (huesSB.length() > 0)
+                        huesSB.append(", ");
+                    huesSB.append(h);
+                }
+                debug("profiles", String.format("used hues: %s", huesSB));
+            }
+            hues.add(hues.get(0));
+
+            int lastHue = -1;
+            int largestInterval = 0;
+            ArrayList<Integer> largestIntervalStarts = new ArrayList<>();
+
+            for (int h : hues) {
+                if (lastHue == -1) {
+                    lastHue = h;
+                    continue;
+                }
+
+                int interval;
+                if (h > lastHue)
+                    interval = h - lastHue;     // 10 -> 20 is a step of 10
+                else
+                    interval = h + (360 - lastHue);    // 350 -> 20 is a step of 30
+
+                if (interval > largestInterval) {
+                    largestInterval = interval;
+                    largestIntervalStarts.clear();
+                    largestIntervalStarts.add(lastHue);
+                }
+                else if (interval == largestInterval) {
+                    largestIntervalStarts.add(lastHue);
+                }
+
+                lastHue = h;
+            }
+
+            final int chosenIndex = (int) (Math.random() * largestIntervalStarts.size());
+            int chosenIntervalStart = largestIntervalStarts.get(chosenIndex);
+
+            debug("profiles",
+                    String.format(Locale.US, "Choosing the middle colour between %d and %d",
+                            chosenIntervalStart, chosenIntervalStart + largestInterval));
+
+            if (largestInterval % 2 != 0)
+                largestInterval++;    // round up the middle point
+
+            chosenHue = (chosenIntervalStart + (largestInterval / 2)) % 360;
+        }
+
+        final int mod = chosenHue % HueRing.hueStepDegrees;
+        if (mod != 0) {
+            if (mod > HueRing.hueStepDegrees / 2)
+                chosenHue += (HueRing.hueStepDegrees - mod); // 13 += (5-3) = 15
+            else
+                chosenHue -= mod;       // 12 -= 2 = 10
+        }
+
+        debug("profiles", String.format(Locale.US, "New profile hue: %d", chosenHue));
+
+        return chosenHue;
+    }
 }
 }
index c4a8b50a0c6684e3280d6849e97b015499ebfc83..cab5008a136d38d3907b498fdcdfe73959915ace 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -82,8 +82,4 @@ public class Digest {
     public int getDigestLength() {
         return digest.getDigestLength();
     }
     public int getDigestLength() {
         return digest.getDigestLength();
     }
-    @Override
-    public Object clone() throws CloneNotSupportedException {
-        return digest.clone();
-    }
 }
 }
index 0a51215a9c85f2797e3b39fdd5b747b36b38632b..d8571bfbf571ec7a12f32302d06fcc9565c69e51 100644 (file)
@@ -25,5 +25,9 @@ public class DimensionUtils {
         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics()));
     }
         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics()));
     }
+    public static int sp2px(Context context, float sp) {
+        return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
+               context.getResources().getDisplayMetrics()));
+    }
 
 }
 
 }
index 56a88ceca9fa70ef8d4f4de91f76c0a16df869eb..4103992bec5903735e3a566fd5ff76132a3affec 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import android.view.inputmethod.InputMethodManager;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Date;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -46,9 +45,9 @@ public final class Globals {
                 }
             };
     public static String[] monthNames;
                 }
             };
     public static String[] monthNames;
-    public static String developerEmail = "dam+mole-crash@ktnx.net";
-    private static Pattern reLedgerDate =
-            Pattern.compile("^(?:(\\d+)/)??(?:(\\d\\d?)/)?(\\d\\d?)$");
+    public static final String developerEmail = "dam+mole-crash@ktnx.net";
+    private static final Pattern reLedgerDate =
+            Pattern.compile("^(?:(?:(\\d+)/)??(\\d\\d?)/)?(\\d\\d?)$");
     public static void hideSoftKeyboard(Activity act) {
         // hide the keyboard
         View v = act.getCurrentFocus();
     public static void hideSoftKeyboard(Activity act) {
         // hide the keyboard
         View v = act.getCurrentFocus();
@@ -58,37 +57,48 @@ public final class Globals {
             imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
         }
     }
             imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
         }
     }
-    public static Date parseLedgerDate(String dateString) throws ParseException {
+    public static SimpleDate parseLedgerDate(String dateString) throws ParseException {
         Matcher m = reLedgerDate.matcher(dateString);
         if (!m.matches()) throw new ParseException(
                 String.format("'%s' does not match expected pattern '%s'", dateString,
                         reLedgerDate.toString()), 0);
 
         Matcher m = reLedgerDate.matcher(dateString);
         if (!m.matches()) throw new ParseException(
                 String.format("'%s' does not match expected pattern '%s'", dateString,
                         reLedgerDate.toString()), 0);
 
-        String year = m.group(1);
-        String month = m.group(2);
-        String day = m.group(3);
+        String yearStr = m.group(1);
+        String monthStr = m.group(2);
+        String dayStr = m.group(3);
+
+        int year, month, day;
 
         String toParse;
 
         String toParse;
-        if (year == null) {
-            Calendar now = Calendar.getInstance();
-            int thisYear = now.get(Calendar.YEAR);
-            if (month == null) {
-                int thisMonth = now.get(Calendar.MONTH) + 1;
-                toParse = String.format(Locale.US, "%04d/%02d/%s", thisYear, thisMonth, dateString);
+        if (yearStr == null) {
+            SimpleDate today = SimpleDate.today();
+            year = today.year;
+            if (monthStr == null) {
+                month = today.month;
             }
             }
-            else toParse = String.format(Locale.US, "%04d/%s", thisYear, dateString);
+            else month = Integer.parseInt(monthStr);
+        }
+        else {
+            year = Integer.parseInt(yearStr);
+            assert monthStr != null;
+            month = Integer.parseInt(monthStr);
         }
         }
-        else toParse = dateString;
 
 
-        return dateFormatter.get().parse(toParse);
+        assert dayStr != null;
+        day = Integer.parseInt(dayStr);
+
+        return new SimpleDate(year, month, day);
+    }
+    public static Calendar parseLedgerDateAsCalendar(String dateString) throws ParseException {
+        return parseLedgerDate(dateString).toCalendar();
     }
     }
-    public static Date parseIsoDate(String dateString) throws ParseException {
-        return isoDateFormatter.get().parse(dateString);
+    public static SimpleDate parseIsoDate(String dateString) throws ParseException {
+        return SimpleDate.fromDate(isoDateFormatter.get().parse(dateString));
     }
     }
-    public static String formatLedgerDate(Date date) {
-        return dateFormatter.get().format(date);
+    public static String formatLedgerDate(SimpleDate date) {
+        return dateFormatter.get().format(date.toDate());
     }
     }
-    public static String formatIsoDate(Date date) {
-        return isoDateFormatter.get().format(date);
+    public static String formatIsoDate(SimpleDate date) {
+        return isoDateFormatter.get().format(date.toDate());
     }
 }
\ No newline at end of file
     }
 }
\ No newline at end of file
index 515b314ca8f10fb74627ee606e855493789a1c0e..097fdd08f3f94dae77fdf4508b9b1c58d858721d 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,8 +19,8 @@ package net.ktnx.mobileledger.utils;
 
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 
-public class Locker {
-    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+public class Locker implements AutoCloseable {
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
     public LockHolder lockForWriting() {
         ReentrantReadWriteLock.WriteLock wLock = lock.writeLock();
         wLock.lock();
     public LockHolder lockForWriting() {
         ReentrantReadWriteLock.WriteLock wLock = lock.writeLock();
         wLock.lock();
@@ -35,4 +35,9 @@ public class Locker {
         rLock.lock();
         return new LockHolder(rLock);
     }
         rLock.lock();
         return new LockHolder(rLock);
     }
+    @Override
+    public void close() {
+        lock.readLock().unlock();
+        lock.writeLock().unlock();
+    }
 }
 }
index 68d3a7af7eed89bbcf97c21348350d65dce54c29..ec7d6efbefc15206f7344edb0e367f51dede47cd 100644 (file)
@@ -28,4 +28,10 @@ public final class Logger {
     public static void debug(String tag, String msg, Throwable e) {
         if (BuildConfig.DEBUG) Log.d(tag, msg, e);
     }
     public static void debug(String tag, String msg, Throwable e) {
         if (BuildConfig.DEBUG) Log.d(tag, msg, e);
     }
+    public static void warn(String tag, String msg) {
+        Log.w(tag, msg);
+    }
+    public static void warn(String tag, String msg, Throwable e) {
+        Log.w(tag, msg, e);
+    }
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java b/app/src/main/java/net/ktnx/mobileledger/utils/MLDB.java
deleted file mode 100644 (file)
index ab792ae..0000000
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- * 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.MatrixCursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.provider.FontsContract;
-import android.widget.AutoCompleteTextView;
-import android.widget.FilterQueryProvider;
-import android.widget.SimpleCursorAdapter;
-
-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 java.util.Locale;
-
-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[] col_names = {FontsContract.Columns._ID, field};
-            MatrixCursor c = new MatrixCursor(col_names);
-
-            String sql;
-            String[] params;
-            if (profileSpecific) {
-                MobileLedgerProfile p = (profile == null) ? Data.profile.getValue() : profile;
-                if (p == null) throw new AssertionError();
-                sql = String.format("SELECT %s as a, 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 2, 1;", field, field, field, field, table, field);
-                params = new String[]{str, str, str, p.getUuid(), str};
-            }
-            else {
-                sql = String.format("SELECT %s as a, 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 2, 1;", field, field, field, field, table, field);
-                params = new String[]{str, str, str, str};
-            }
-            debug("autocompletion", sql);
-            SQLiteDatabase db = App.getDatabase();
-
-            try (Cursor matches = db.rawQuery(sql, params)) {
-                int i = 0;
-                while (matches.moveToNext()) {
-                    String match = matches.getString(0);
-                    int order = matches.getInt(1);
-                    debug("autocompletion",
-                            String.format(Locale.ENGLISH, "match: %s |%d", match, order));
-                    c.newRow().add(i++).add(match);
-                }
-            }
-
-            return c;
-
-        };
-
-        adapter.setFilterQueryProvider(provider);
-
-        view.setAdapter(adapter);
-
-        if (callback != null) view.setOnItemClickListener((parent, itemView, position, id) -> {
-            callback.descriptionSelected(String.valueOf(view.getText()));
-        });
-    }
-}
-
index 9e0a3fec2cc66ae312103cd65b7b6adcd033a2a1..67dc0958686a9722f15b3f797d3634740c531dbc 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,15 +19,23 @@ package net.ktnx.mobileledger.utils;
 
 import android.app.Activity;
 import android.content.res.Configuration;
 
 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 android.view.WindowManager;
 
+import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentActivity;
 
+import org.jetbrains.annotations.Contract;
+
 public class Misc {
 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 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 void showSoftKeyboard(Activity activity) {
         // make the keyboard appear
         Configuration cf = activity.getResources()
@@ -56,4 +64,66 @@ public class Misc {
                     .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
 
     }
                     .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
 
     }
+    public static String emptyIsNull(String 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());
+    }
+    public static boolean equalStrings(String a, String b) {
+        return nullIsEmpty(a).equals(nullIsEmpty(b));
+    }
+    public static String trim(@Nullable String string) {
+        if (string == null)
+            return null;
+
+        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();
+    }
 }
 }
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java b/app/src/main/java/net/ktnx/mobileledger/utils/MobileLedgerDatabase.java
deleted file mode 100644 (file)
index 9d841ea..0000000
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.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 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 {
-    private static final String DB_NAME = "MoLe.db";
-    private static final int LATEST_REVISION = 24;
-    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 onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-        debug("db", "onUpgrade called");
-        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();
-        }
-    }
-}
index c0802a4547b1f0668faf3810f4852f668155649f..97c09911f60a6aa5547eef8d12512c01f70b777a 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * 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;
 
 
 package net.ktnx.mobileledger.utils;
 
-import net.ktnx.mobileledger.model.MobileLedgerProfile;
+import androidx.annotation.NonNull;
+
+import net.ktnx.mobileledger.db.Profile;
+
+import org.jetbrains.annotations.NotNull;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
@@ -27,14 +31,19 @@ import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public final class NetworkUtil {
     private static final int thirtySeconds = 30000;
 
 public final class NetworkUtil {
     private static final int thirtySeconds = 30000;
-    public static HttpURLConnection prepareConnection(MobileLedgerProfile profile, String path)
-            throws IOException {
-        String url = profile.getUrl();
-        final boolean use_auth = profile.isAuthEnabled();
-        if (!url.endsWith("/")) url += "/";
-        url += path;
-        debug("network", "Connecting to " + url);
-        HttpURLConnection http = (HttpURLConnection) new URL(url).openConnection();
+    @NotNull
+    public static HttpURLConnection prepareConnection(@NonNull Profile profile,
+                                                      @NonNull String path) throws IOException {
+        return prepareConnection(profile.getUrl(), path, profile.useAuthentication());
+    }
+    public static HttpURLConnection prepareConnection(@NonNull String url, @NonNull String path,
+                                                      boolean authEnabled) throws IOException {
+        String connectURL = url;
+        if (!connectURL.endsWith("/"))
+            connectURL += "/";
+        connectURL += path;
+        debug("network", "Connecting to " + connectURL);
+        HttpURLConnection http = (HttpURLConnection) new URL(connectURL).openConnection();
         http.setAllowUserInteraction(true);
         http.setRequestProperty("Accept-Charset", "UTF-8");
         http.setInstanceFollowRedirects(false);
         http.setAllowUserInteraction(true);
         http.setRequestProperty("Accept-Charset", "UTF-8");
         http.setInstanceFollowRedirects(false);
index d4900e0b2ccb9b4fb63ceb9d06f6a58abe19da01..f804464cb041bff358d254b1cb607f9b7ed0f409 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,15 +19,15 @@ package net.ktnx.mobileledger.utils;
 
 import android.os.Build;
 
 
 import android.os.Build;
 
+import androidx.annotation.RequiresApi;
+
 import java.util.Observable;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.IntBinaryOperator;
 import java.util.function.IntUnaryOperator;
 
 import java.util.Observable;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.IntBinaryOperator;
 import java.util.function.IntUnaryOperator;
 
-import androidx.annotation.RequiresApi;
-
 public class ObservableAtomicInteger extends Observable {
 public class ObservableAtomicInteger extends Observable {
-    private AtomicInteger holder;
+    private final AtomicInteger holder;
     ObservableAtomicInteger() {
         super();
         holder = new AtomicInteger();
     ObservableAtomicInteger() {
         super();
         holder = new AtomicInteger();
index 363a8a1a053f00ae1b18fe611091e867f01e2aef..4ca3e695ae0a5d2cb9595fcd337fb62688129a6e 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -19,6 +19,10 @@ package net.ktnx.mobileledger.utils;
 
 import android.os.Build;
 
 
 import android.os.Build;
 
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
@@ -34,15 +38,11 @@ import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
 import java.util.stream.Stream;
 
 import java.util.function.UnaryOperator;
 import java.util.stream.Stream;
 
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class ObservableList<T> extends Observable implements List<T> {
     private List<T> list;
 import static net.ktnx.mobileledger.utils.Logger.debug;
 
 public class ObservableList<T> extends Observable implements List<T> {
     private List<T> list;
-    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
     private int notificationBlocks = 0;
     private boolean notificationWasBlocked = false;
     public ObservableList(List<T> list) {
     private int notificationBlocks = 0;
     private boolean notificationWasBlocked = false;
     public ObservableList(List<T> list) {
@@ -81,11 +81,14 @@ public class ObservableList<T> extends Observable implements List<T> {
         throw new RuntimeException("Iterators break encapsulation and ignore locking");
 //        return list.iterator();
     }
         throw new RuntimeException("Iterators break encapsulation and ignore locking");
 //        return list.iterator();
     }
+    @NotNull
     public Object[] toArray() {
         try (LockHolder lh = lockForReading()) {
             return list.toArray();
         }
     }
     public Object[] toArray() {
         try (LockHolder lh = lockForReading()) {
             return list.toArray();
         }
     }
+    @Override
+    @NotNull
     public <T1> T1[] toArray(@Nullable T1[] a) {
         try (LockHolder lh = lockForReading()) {
             return list.toArray(a);
     public <T1> T1[] toArray(@Nullable T1[] a) {
         try (LockHolder lh = lockForReading()) {
             return list.toArray(a);
@@ -95,7 +98,8 @@ public class ObservableList<T> extends Observable implements List<T> {
         try (LockHolder lh = lockForWriting()) {
             boolean result = list.add(t);
             lh.downgrade();
         try (LockHolder lh = lockForWriting()) {
             boolean result = list.add(t);
             lh.downgrade();
-            if (result) forceNotify();
+            if (result)
+                forceNotify();
             return result;
         }
     }
             return result;
         }
     }
@@ -234,40 +238,46 @@ public class ObservableList<T> extends Observable implements List<T> {
     @NotNull
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Spliterator<T> spliterator() {
     @NotNull
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Spliterator<T> spliterator() {
-        if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
-                "Iterators break encapsulation and ignore locking. Write-lock first");
+        if (!lock.isWriteLockedByCurrentThread())
+            throw new RuntimeException(
+                    "Iterators break encapsulation and ignore locking. Write-lock first");
         return list.spliterator();
     }
     @RequiresApi(api = Build.VERSION_CODES.N)
         return list.spliterator();
     }
     @RequiresApi(api = Build.VERSION_CODES.N)
-    public boolean removeIf(Predicate<? super T> filter) {
+    public boolean removeIf(@NotNull Predicate<? super T> filter) {
         try (LockHolder lh = lockForWriting()) {
             boolean result = list.removeIf(filter);
             lh.downgrade();
         try (LockHolder lh = lockForWriting()) {
             boolean result = list.removeIf(filter);
             lh.downgrade();
-            if (result) forceNotify();
+            if (result)
+                forceNotify();
             return result;
         }
     }
             return result;
         }
     }
+    @NotNull
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Stream<T> stream() {
         if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
                 "Iterators break encapsulation and ignore locking. Write-lock first");
         return list.stream();
     }
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Stream<T> stream() {
         if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
                 "Iterators break encapsulation and ignore locking. Write-lock first");
         return list.stream();
     }
+    @NotNull
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Stream<T> parallelStream() {
     @RequiresApi(api = Build.VERSION_CODES.N)
     public Stream<T> parallelStream() {
-        if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
-                "Iterators break encapsulation and ignore locking. Write-lock first");
+        if (!lock.isWriteLockedByCurrentThread())
+            throw new RuntimeException(
+                    "Iterators break encapsulation and ignore locking. Write-lock first");
         return list.parallelStream();
     }
     @RequiresApi(api = Build.VERSION_CODES.N)
         return list.parallelStream();
     }
     @RequiresApi(api = Build.VERSION_CODES.N)
-    public void forEach(Consumer<? super T> action) {
+    public void forEach(@NotNull Consumer<? super T> action) {
         try (LockHolder lh = lockForReading()) {
             list.forEach(action);
         }
     }
     public List<T> getList() {
         try (LockHolder lh = lockForReading()) {
             list.forEach(action);
         }
     }
     public List<T> getList() {
-        if (!lock.isWriteLockedByCurrentThread()) throw new RuntimeException(
-                "Direct list access breaks encapsulation and ignore locking. Write-lock first");
+        if (!lock.isWriteLockedByCurrentThread())
+            throw new RuntimeException(
+                    "Direct list access breaks encapsulation and ignore locking. Write-lock first");
         return list;
     }
     public void setList(List<T> aList) {
         return list;
     }
     public void setList(List<T> aList) {
index 393a9cdce68ec3dfa864e98462b0c9e24df58599..326be07207c3fe84f177fba60b090f6ba5587ded 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -41,7 +41,7 @@ public class ObservableValue<T> {
     public void notifyObservers() {
         impl.notifyObservers();
     }
     public void notifyObservers() {
         impl.notifyObservers();
     }
-    public void notifyObservers(Object arg) {
+    public void notifyObservers(T arg) {
         impl.notifyObservers(arg);
     }
     public void deleteObservers() {
         impl.notifyObservers(arg);
     }
     public void deleteObservers() {
@@ -57,7 +57,7 @@ public class ObservableValue<T> {
         impl.setChanged();
         impl.notifyObservers();
     }
         impl.setChanged();
         impl.notifyObservers();
     }
-    private class ObservableValueImpl<T> extends Observable {
+    private static class ObservableValueImpl<T> extends Observable {
         protected T value;
         public void setValue(T newValue) {
             setValue(newValue, true);
         protected T value;
         public void setValue(T newValue) {
             setValue(newValue, true);
@@ -66,7 +66,8 @@ public class ObservableValue<T> {
             super.setChanged();
         }
         private synchronized void setValue(T newValue, boolean notify) {
             super.setChanged();
         }
         private synchronized void setValue(T newValue, boolean notify) {
-            if ((newValue == null) && (value == null)) return;
+            if ((newValue == null) && (value == null))
+                return;
 
             if ((newValue != null) && newValue.equals(value)) return;
 
 
             if ((newValue != null) && newValue.equals(value)) return;
 
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/Profiler.java b/app/src/main/java/net/ktnx/mobileledger/utils/Profiler.java
new file mode 100644 (file)
index 0000000..1545090
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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));
+    }
+}
diff --git a/app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java b/app/src/main/java/net/ktnx/mobileledger/utils/SimpleDate.java
new file mode 100644 (file)
index 0000000..52db9bd
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class SimpleDate implements Comparable<SimpleDate> {
+    public final int year;
+    public final int month;
+    public final int day;
+    public SimpleDate(int y, int m, int d) {
+        year = y;
+        month = m;
+        day = d;
+    }
+    public static SimpleDate fromDate(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        return fromCalendar(calendar);
+    }
+    public static SimpleDate fromCalendar(Calendar calendar) {
+        return new SimpleDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1,
+                calendar.get(Calendar.DATE));
+    }
+    public static SimpleDate today() {
+        return fromCalendar(Calendar.getInstance());
+    }
+    public Calendar toCalendar() {
+        Calendar result = Calendar.getInstance();
+        result.set(year, month - 1, day);
+        return result;
+    }
+    public Date toDate() {
+        return toCalendar().getTime();
+    }
+    public boolean equals(@Nullable SimpleDate date) {
+        if (date == null)
+            return false;
+
+        return ((year == date.year) && (month == date.month) && (day == date.day));
+    }
+    public boolean earlierThan(@NonNull SimpleDate date) {
+        if (year < date.year)
+            return true;
+        if (year > date.year)
+            return false;
+        if (month < date.month)
+            return true;
+        if (month > date.month)
+            return false;
+        return (day < date.day);
+    }
+    public boolean laterThan(@NonNull SimpleDate date) {
+        if (year > date.year)
+            return true;
+        if (year < date.year)
+            return false;
+        if (month > date.month)
+            return true;
+        if (month < date.month)
+            return false;
+        return (day > date.day);
+    }
+    public int compareTo(SimpleDate date) {
+        int res = Integer.compare(year, date.year);
+        if (res != 0)
+            return res;
+
+        res = Integer.compare(month, date.month);
+        if (res != 0)
+            return res;
+
+        return Integer.compare(day, date.day);
+    }
+    public Calendar asCalendar() {
+        final Calendar calendar = Calendar.getInstance();
+        calendar.set(year, month, day);
+        return calendar;
+    }
+    public String toString() {
+        return String.format(Locale.US, "%d-%02d-%02d", year, month, day);
+    }
+}
index f010e4c17451526d41e75200f02c0b159f6479ce..40c104eedbf820dc4c1ebd588664ad773f205963 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -26,7 +26,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 public class UrlEncodedFormData {
 import java.util.List;
 
 public class UrlEncodedFormData {
-    private List<AbstractMap.SimpleEntry<String,String>> pairs;
+    private final List<AbstractMap.SimpleEntry<String, String>> pairs;
 
     public UrlEncodedFormData() {
         pairs = new ArrayList<>();
 
     public UrlEncodedFormData() {
         pairs = new ArrayList<>();
diff --git a/app/src/main/res/anim/fade_in_slowly.xml b/app/src/main/res/anim/fade_in_slowly.xml
new file mode 100644 (file)
index 0000000..1c76bb1
--- /dev/null
@@ -0,0 +1,25 @@
+<?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/>.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="500"
+    >
+    <alpha
+        android:fromAlpha="0.0"
+        android:toAlpha="1.0"
+        />
+</set>
\ No newline at end of file
diff --git a/app/src/main/res/anim/fade_out_slowly.xml b/app/src/main/res/anim/fade_out_slowly.xml
new file mode 100644 (file)
index 0000000..296c095
--- /dev/null
@@ -0,0 +1,25 @@
+<?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/>.
+  -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="500"
+    >
+    <alpha
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"
+        />
+</set>
\ No newline at end of file
diff --git a/app/src/main/res/anim/layout_slide_down.xml b/app/src/main/res/anim/layout_slide_down.xml
deleted file mode 100644 (file)
index a91dafc..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/rotate_180.xml b/app/src/main/res/anim/rotate_180.xml
deleted file mode 100644 (file)
index abe8113..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/rotate_180_back.xml b/app/src/main/res/anim/rotate_180_back.xml
deleted file mode 100644 (file)
index 87761a0..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/slide_down.xml b/app/src/main/res/anim/slide_down.xml
deleted file mode 100644 (file)
index 5c3f8ae..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml
deleted file mode 100644 (file)
index 9ffc9e6..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml
deleted file mode 100644 (file)
index 1cb9ff2..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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
diff --git a/app/src/main/res/anim/slide_up.xml b/app/src/main/res/anim/slide_up.xml
deleted file mode 100644 (file)
index 0e2ce6d..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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
diff --git a/app/src/main/res/drawable-anydpi-v21/app_icon.xml b/app/src/main/res/drawable-anydpi-v21/app_icon.xml
deleted file mode 100644 (file)
index a696e76..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="100"
-    android:viewportHeight="100">
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#935ff2"
-        android:pathData="M8,0L92,0A8,8 0,0 1,100 8L100,92A8,8 0,0 1,92 100L8,100A8,8 0,0 1,0 92L0,8A8,8 0,0 1,8 0z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#00000000"
-        android:pathData="m40.908,32.332a12.857,12.859 0,0 1,18.183 0"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#00000000"
-        android:pathData="m31.817,23.768a25.715,25.718 0,0 1,36.366 0"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#00000000"
-        android:pathData="m15.658,40.914c0,0 10.48,-1.461 15.755,-1.499 6.217,-0.045 18.593,1.499 18.593,1.499 0,0 12.821,-1.568 19.26,-1.499 5.05,0.054 15.077,1.499 15.077,1.499 0.914,0.087 1.658,0.74 1.658,1.66v41.657c0,0.919 -0.74,1.66 -1.658,1.66 0,0 -10.026,-1.495 -15.077,-1.548 -6.441,-0.067 -19.26,1.571 -19.26,1.571 0,0 -12.374,-1.614 -18.593,-1.571 -5.276,0.036 -15.755,1.548 -15.755,1.548 -0.919,0 -1.658,-0.74 -1.658,-1.66v-41.657c0,-0.919 0.74,-1.66 1.658,-1.66z"
-        android:strokeWidth="4"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillColor="#00000000"
-        android:fillType="evenOdd"
-        android:pathData="M50,84.75L50,40.914"
-        android:strokeWidth="4.00000048"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="butt"
-        android:strokeLineJoin="miter" />
-    <path
-        android:fillColor="#00000000"
-        android:fillType="evenOdd"
-        android:pathData="m19.701,64.315h25"
-        android:strokeWidth="4.9000001"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="butt"
-        android:strokeLineJoin="miter" />
-    <path
-        android:fillColor="#00000000"
-        android:fillType="evenOdd"
-        android:pathData="m32.201,76.815v-25"
-        android:strokeWidth="4.9000001"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="butt"
-        android:strokeLineJoin="miter" />
-    <path
-        android:fillColor="#00000000"
-        android:fillType="evenOdd"
-        android:pathData="m55.105,64.315h25"
-        android:strokeWidth="4.9000001"
-        android:strokeAlpha="1"
-        android:strokeColor="#ffffff"
-        android:strokeLineCap="butt"
-        android:strokeLineJoin="miter" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#935ff2"
-        android:fillType="nonZero"
-        android:pathData="m50,32.266c-4.704,0 -8.629,3.927 -8.629,8.631 0,4.704 3.925,8.629 8.629,8.629 4.704,0 8.629,-3.925 8.629,-8.629 0,-4.704 -3.925,-8.631 -8.629,-8.631z"
-        android:strokeWidth="10.39999962"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:fillType="nonZero"
-        android:pathData="m50,37.647c1.683,0 3.25,1.569 3.25,3.25 0,1.681 -1.567,3.246 -3.25,3.246 -1.683,0 -3.25,-1.566 -3.25,-3.246 0,-1.681 1.567,-3.25 3.25,-3.25z"
-        android:strokeWidth="10.39999962"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/app_icon_dynamic.xml b/app/src/main/res/drawable-anydpi-v21/app_icon_dynamic.xml
deleted file mode 100644 (file)
index e99168b..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="100"
-    android:viewportHeight="100">
-    <!--path
-        android:fillAlpha="1"
-        android:fillColor="?colorPrimary"
-        android:pathData="m8,0h84c4.432,0 8,3.568 8,8v84c0,4.432 -3.568,8 -8,8H8C3.568,100 0,96.432 0,92V8C0,3.568 3.568,0 8,0Z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" /-->
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m54,38.688a4,4 0,0 1,-4 4,4 4,0 0,1 -4,-4 4,4 0,0 1,4 -4,4 4,0 0,1 4,4z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m27.27,35.693c-6.756,0.048 -19.33,1.81 -19.462,1.828 0.076,-0.004 0.152,-0.012 0.229,-0.012l-0.273,0.018c0,0 0.043,-0.006 0.044,-0.006 -2.084,0.122 -3.796,1.878 -3.796,3.996v50.443c0,2.195 1.838,4.01 4.026,4.01a2,2 0,0 0,0.283 -0.02c0,0 12.891,-1.813 18.977,-1.854 7.237,-0.049 22.447,1.887 22.447,1.887a2,2 0,0 0,0.507 0c0,0 15.753,-1.964 23.258,-1.887 5.808,0.06 18.148,1.852 18.148,1.852a2,2 0,0 0,0.291 0.022c2.187,0 4.025,-1.815 4.025,-4.01v-50.443c0,-2.201 -1.89,-3.816 -3.838,-4 -0.167,-0.024 -12.112,-1.755 -18.586,-1.824 -4.371,-0.046 -10.704,0.465 -15.724,0.956l0.393,3.992c4.948,-0.482 11.21,-0.992 15.29,-0.948 5.819,0.062 18.157,1.795 18.157,1.795a2,2 0,0 0,0.096 0.011c0.287,0.027 0.213,-0.008 0.213,0.018v50.44c-0.456,-0.068 -12.016,-1.793 -18.424,-1.859 -6.628,-0.069 -17.426,1.15 -21.551,1.646v-45.384h-4v45.378c-4.07,-0.499 -14.381,-1.683 -20.728,-1.64 -6.685,0.045 -18.819,1.797 -19.26,1.861v-50.441c0,-0.031 -0.032,-0.008 0.025,-0.008a2,2 0,0 0,0.273 -0.02c0,0 12.891,-1.753 18.988,-1.797 3.778,-0.027 9.522,0.442 14.207,0.903l0.521,-3.98c-4.773,-0.472 -10.66,-0.952 -14.757,-0.923z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m50,19.713c-4.871,0 -9.742,1.848 -13.436,5.541a4,4 0,1 0,5.658 5.656c4.329,-4.329 11.225,-4.329 15.555,0a4,4 0,1 0,5.658 -5.656C59.742,21.561 54.871,19.713 50,19.713Z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m50,4.713c-8.71,0 -17.419,3.311 -24.041,9.934a4,4 0,1 0,5.656 5.656c10.187,-10.187 26.582,-10.187 36.77,0a4,4 0,1 0,5.656 -5.656C67.419,8.024 58.71,4.713 50,4.713Z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m56.574,62.805v8h30.852v-8z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-    <path
-        android:fillAlpha="1"
-        android:fillColor="#ffffff"
-        android:pathData="m23.985,51.5v11.321h-11.411v7.968h11.411v11.32h8.032v-11.32h11.411v-7.968L32.016,62.82L32.016,51.5Z"
-        android:strokeWidth="6.5"
-        android:strokeAlpha="1"
-        android:strokeColor="#00000000"
-        android:strokeLineCap="round"
-        android:strokeLineJoin="round" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/checkbox_star_black.xml b/app/src/main/res/drawable-anydpi-v21/checkbox_star_black.xml
deleted file mode 100644 (file)
index c11c0a4..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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/>.
-  -->
-
-<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
diff --git a/app/src/main/res/drawable-anydpi-v21/checkbox_star_white.xml b/app/src/main/res/drawable-anydpi-v21/checkbox_star_white.xml
deleted file mode 100644 (file)
index 31e150c..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-<?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/>.
-  -->
-
-<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
diff --git a/app/src/main/res/drawable-anydpi-v21/dashed_border_1dp.xml b/app/src/main/res/drawable-anydpi-v21/dashed_border_1dp.xml
deleted file mode 100644 (file)
index c9e5e96..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="line"
-    android:thickness="1dp"
-    android:tint="?colorAccent">
-    <stroke
-        android:width="1dp"
-        android:color="?colorAccent"
-        android:dashWidth="2dp"
-        android:dashGap="6dp" />
-
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/dashed_border_8dp.xml b/app/src/main/res/drawable-anydpi-v21/dashed_border_8dp.xml
deleted file mode 100644 (file)
index 884aed2..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="line"
-    android:thickness="1dp"
-    android:tint="?colorAccent">
-    <stroke
-        android:width="8dp"
-        android:color="?colorAccent"
-        android:dashWidth="16dp"
-        android:dashGap="8dp" />
-
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/drop_shadow.xml b/app/src/main/res/drawable-anydpi-v21/drop_shadow.xml
deleted file mode 100644 (file)
index ab8aace..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <gradient
-        android:startColor="#80000000"
-        android:endColor="#00000000"
-        android:angle="270"
-        />
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/fade_down_white.xml b/app/src/main/res/drawable-anydpi-v21/fade_down_white.xml
deleted file mode 100644 (file)
index 63add29..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <gradient
-        android:startColor="#40ffffff"
-        android:endColor="#FFffffff"
-        android:angle="270"
-        />
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_add_circle_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_add_circle_white_24dp.xml
deleted file mode 100644 (file)
index 5ca1523..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorPrimary"
-    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,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_add_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_add_white_24dp.xml
deleted file mode 100644 (file)
index a3d2204..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_assignment_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_assignment_black_24dp.xml
deleted file mode 100644 (file)
index 1bb5374..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_cancel_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_cancel_white_24dp.xml
deleted file mode 100644 (file)
index 0a792f4..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_check_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_check_white_24dp.xml
deleted file mode 100644 (file)
index 709f8ac..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_clear_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_clear_black_24dp.xml
deleted file mode 100644 (file)
index 51c798a..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_delete_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_delete_white_24dp.xml
deleted file mode 100644 (file)
index b16500b..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_event_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_event_black_24dp.xml
deleted file mode 100644 (file)
index 3c23e88..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_event_gray_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_event_gray_24dp.xml
deleted file mode 100644 (file)
index 4bda6d5..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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="#FF606060"
-    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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_event_note_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_event_note_black_24dp.xml
deleted file mode 100644 (file)
index 434d09a..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M17,10L7,10v2h10v-2zM19,3h-1L18,1h-2v2L8,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,-2zM19,19L5,19L5,8h14v11zM14,14L7,14v2h7v-2z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_event_primary_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_event_primary_24dp.xml
deleted file mode 100644 (file)
index 48b0450..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_exit_to_app_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_exit_to_app_black_24dp.xml
deleted file mode 100644 (file)
index 8c390b3..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_expand_less_black_24dp.xml
deleted file mode 100644 (file)
index 14e4ec1..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_expand_more_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_expand_more_black_24dp.xml
deleted file mode 100644 (file)
index 32441e4..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_filter_list_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_filter_list_black_24dp.xml
deleted file mode 100644 (file)
index 9eeb1cd..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorPrimary"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_filter_list_white_24dp.xml
deleted file mode 100644 (file)
index 336d3cc..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_home_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_home_black_24dp.xml
deleted file mode 100644 (file)
index 3796e51..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_info_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_info_black_24dp.xml
deleted file mode 100644 (file)
index 09fd157..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_keyboard_arrow_down_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_keyboard_arrow_down_black_24dp.xml
deleted file mode 100644 (file)
index d52eacf..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_launcher_background.xml b/app/src/main/res/drawable-anydpi-v21/ic_launcher_background.xml
deleted file mode 100644 (file)
index fdef843..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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="108dp"
-    android:height="108dp"
-    android:viewportWidth="108"
-    android:viewportHeight="108">
-    <path
-        android:fillColor="#008577"
-        android:pathData="M0,0h108v108h-108z" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M9,0L9,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,0L19,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,0L29,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,0L39,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,0L49,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,0L59,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,0L69,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,0L79,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M89,0L89,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M99,0L99,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,9L108,9"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,19L108,19"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,29L108,29"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,39L108,39"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,49L108,49"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,59L108,59"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,69L108,69"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,79L108,79"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,89L108,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,99L108,99"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,29L89,29"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,39L89,39"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,49L89,49"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,59L89,59"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,69L89,69"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,79L89,79"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,19L29,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,19L39,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,19L49,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,19L59,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,19L69,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,19L79,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_menu_manage.xml b/app/src/main/res/drawable-anydpi-v21/ic_menu_manage.xml
deleted file mode 100644 (file)
index 830aa28..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-  ~ 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
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_menu_send.xml b/app/src/main/res/drawable-anydpi-v21/ic_menu_send.xml
deleted file mode 100644 (file)
index 476e674..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_menu_share.xml b/app/src/main/res/drawable-anydpi-v21/ic_menu_share.xml
deleted file mode 100644 (file)
index f83b2ec..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_mode_edit_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_mode_edit_black_24dp.xml
deleted file mode 100644 (file)
index bb0a23a..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_more_horiz_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_more_horiz_black_24dp.xml
deleted file mode 100644 (file)
index f830d8d..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_notifications_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_notifications_black_24dp.xml
deleted file mode 100644 (file)
index dcbcf36..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_palette_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_palette_black_24dp.xml
deleted file mode 100644 (file)
index 9f6ceae..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_refresh_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_refresh_white_24dp.xml
deleted file mode 100644 (file)
index 94377a1..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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="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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_save_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_save_white_24dp.xml
deleted file mode 100644 (file)
index 7429776..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_settings_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_settings_black_24dp.xml
deleted file mode 100644 (file)
index 9a43c8a..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    android:viewportWidth="24.0"
-    android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
-</vector>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_star_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_star_black_24dp.xml
deleted file mode 100644 (file)
index 276471f..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_star_border_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_star_border_black_24dp.xml
deleted file mode 100644 (file)
index 3dae60a..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_star_border_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_star_border_white_24dp.xml
deleted file mode 100644 (file)
index 77acbe0..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_star_white_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_star_white_24dp.xml
deleted file mode 100644 (file)
index b3c879e..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_sync_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_sync_black_24dp.xml
deleted file mode 100644 (file)
index 24bde51..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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,-.25 1.97,-.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.25,-1.97.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
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_thick_check_white.xml b/app/src/main/res/drawable-anydpi-v21/ic_thick_check_white.xml
deleted file mode 100644 (file)
index f427e07..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<!--
-  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_unfold_more_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_unfold_more_black_24dp.xml
deleted file mode 100644 (file)
index 7d836a1..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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>
diff --git a/app/src/main/res/drawable-anydpi-v21/ic_view_list_black_24dp.xml b/app/src/main/res/drawable-anydpi-v21/ic_view_list_black_24dp.xml
deleted file mode 100644 (file)
index a303787..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<!--
-  ~ 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="?colorAccent"
-    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>
diff --git a/app/src/main/res/drawable-anydpi-v21/list_divider.xml b/app/src/main/res/drawable-anydpi-v21/list_divider.xml
deleted file mode 100644 (file)
index 3dea8eb..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android">
-    <gradient
-        android:type="linear"
-        android:endColor="?colorPrimaryTransparent"
-        android:centerColor="?colorPrimary"
-        android:startColor="?colorPrimaryTransparent" />
-    <size android:height="2dp" />
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/list_divider_inside_out.xml b/app/src/main/res/drawable-anydpi-v21/list_divider_inside_out.xml
deleted file mode 100644 (file)
index 5351906..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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/>.
-  -->
-
-<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
diff --git a/app/src/main/res/drawable-anydpi-v21/side_nav_bar.xml b/app/src/main/res/drawable-anydpi-v21/side_nav_bar.xml
deleted file mode 100644 (file)
index 1ffc746..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-    android:shape="rectangle">
-    <solid
-        android:color="?colorPrimary"
-        />
-</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v21/svg_thick_plus_white.xml b/app/src/main/res/drawable-anydpi-v21/svg_thick_plus_white.xml
deleted file mode 100644 (file)
index ca2839e..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-<?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/>.
-  -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="6.4"
-    android:viewportHeight="6.4">
-    <group
-        android:name="layer1"
-        android:translateX="0"
-        android:translateY="-290.65">
-        <path
-            android:fillColor="#FFFFFF"
-            android:pathData="m 3.1750002 290.64999 c -0.5675775 0 -1.0245037 0.45692 -1.0245037 1.0245 v 1.12599 H 1.0245036 c -0.56757673 0 -1.024503652878986 0.45693 -1.024503652878986 1.02451 0 0.56757 0.456926922878986 1.0245 1.024503652878986 1.0245 h 1.1259929 v 1.12599 c 0 0.56758 0.4569262 1.02451 1.0245037 1.02451 0.5675774 0 1.0245036 -0.45693 1.0245036 -1.02451 v -1.12599 h 1.1259929 c 0.5675768 0 1.0245034 -0.45693 1.0245034 -1.0245 0 -0.56758 -0.4569266 -1.02451 -1.0245034 -1.02451 H 4.1995038 v -1.12599 c 0 -0.56758 -0.4569262 -1.0245 -1.0245036 -1.0245 z" />
-    </group>
-</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v26/app_icon.xml b/app/src/main/res/drawable-anydpi-v26/app_icon.xml
new file mode 100644 (file)
index 0000000..b82ac92
--- /dev/null
@@ -0,0 +1,22 @@
+<?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/>.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi-v26/app_icon_round.xml b/app/src/main/res/drawable-anydpi-v26/app_icon_round.xml
new file mode 100644 (file)
index 0000000..b82ac92
--- /dev/null
@@ -0,0 +1,22 @@
+<?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/>.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/app_icon.xml b/app/src/main/res/drawable-anydpi/app_icon.xml
new file mode 100644 (file)
index 0000000..9d08e3e
--- /dev/null
@@ -0,0 +1,115 @@
+<!--
+  ~ 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:viewportWidth="100"
+    android:viewportHeight="100">
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#935ff2"
+        android:pathData="M8,0L92,0A8,8 0,0 1,100 8L100,92A8,8 0,0 1,92 100L8,100A8,8 0,0 1,0 92L0,8A8,8 0,0 1,8 0z"
+        android:strokeWidth="6.5"
+        android:strokeAlpha="1"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#00000000"
+        android:pathData="m40.908,32.332a12.857,12.859 0,0 1,18.183 0"
+        android:strokeWidth="6.5"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#00000000"
+        android:pathData="m31.817,23.768a25.715,25.718 0,0 1,36.366 0"
+        android:strokeWidth="6.5"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#00000000"
+        android:pathData="m15.658,40.914c0,0 10.48,-1.461 15.755,-1.499 6.217,-0.045 18.593,1.499 18.593,1.499 0,0 12.821,-1.568 19.26,-1.499 5.05,0.054 15.077,1.499 15.077,1.499 0.914,0.087 1.658,0.74 1.658,1.66v41.657c0,0.919 -0.74,1.66 -1.658,1.66 0,0 -10.026,-1.495 -15.077,-1.548 -6.441,-0.067 -19.26,1.571 -19.26,1.571 0,0 -12.374,-1.614 -18.593,-1.571 -5.276,0.036 -15.755,1.548 -15.755,1.548 -0.919,0 -1.658,-0.74 -1.658,-1.66v-41.657c0,-0.919 0.74,-1.66 1.658,-1.66z"
+        android:strokeWidth="4"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="M50,84.75L50,40.914"
+        android:strokeWidth="4.00000048"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="butt"
+        android:strokeLineJoin="miter" />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="m19.701,64.315h25"
+        android:strokeWidth="4.9000001"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="butt"
+        android:strokeLineJoin="miter" />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="m32.201,76.815v-25"
+        android:strokeWidth="4.9000001"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="butt"
+        android:strokeLineJoin="miter" />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="m55.105,64.315h25"
+        android:strokeWidth="4.9000001"
+        android:strokeAlpha="1"
+        android:strokeColor="#ffffff"
+        android:strokeLineCap="butt"
+        android:strokeLineJoin="miter" />
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#935ff2"
+        android:fillType="nonZero"
+        android:pathData="m50,32.266c-4.704,0 -8.629,3.927 -8.629,8.631 0,4.704 3.925,8.629 8.629,8.629 4.704,0 8.629,-3.925 8.629,-8.629 0,-4.704 -3.925,-8.631 -8.629,-8.631z"
+        android:strokeWidth="10.39999962"
+        android:strokeAlpha="1"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+    <path
+        android:fillAlpha="1"
+        android:fillColor="#ffffff"
+        android:fillType="nonZero"
+        android:pathData="m50,37.647c1.683,0 3.25,1.569 3.25,3.25 0,1.681 -1.567,3.246 -3.25,3.246 -1.683,0 -3.25,-1.566 -3.25,-3.246 0,-1.681 1.567,-3.25 3.25,-3.25z"
+        android:strokeWidth="10.39999962"
+        android:strokeAlpha="1"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="round" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/dashed_border_8dp.xml b/app/src/main/res/drawable-anydpi/dashed_border_8dp.xml
new file mode 100644 (file)
index 0000000..aaa93b6
--- /dev/null
@@ -0,0 +1,30 @@
+<?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="8dp"
+        android:color="?colorSecondary"
+        android:dashWidth="16dp"
+        android:dashGap="8dp"
+        />
+
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/drop_shadow.xml b/app/src/main/res/drawable-anydpi/drop_shadow.xml
new file mode 100644 (file)
index 0000000..4eae334
--- /dev/null
@@ -0,0 +1,24 @@
+<?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:startColor="?attr/shadowStartColor"
+        android:endColor="?attr/shadowEndColor"
+        android:angle="270"
+        />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/fade_down_white.xml b/app/src/main/res/drawable-anydpi/fade_down_white.xml
new file mode 100644 (file)
index 0000000..fb22e0c
--- /dev/null
@@ -0,0 +1,25 @@
+<?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:endColor="?android:attr/colorBackground"
+        android:startColor="@android:color/transparent"
+        android:type="linear"
+        android:angle="270"
+        />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/ic_add_circle_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_add_circle_white_24dp.xml
new file mode 100644 (file)
index 0000000..5ca1523
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="?colorPrimary"
+    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,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_add_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_add_white_24dp.xml
new file mode 100644 (file)
index 0000000..9136eb7
--- /dev/null
@@ -0,0 +1,27 @@
+<!--
+  ~ 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="?attr/colorOnSecondary"
+        android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_assignment_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_assignment_black_24dp.xml
new file mode 100644 (file)
index 0000000..5da24bc
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_auto_graph_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_auto_graph_24.xml
new file mode 100644 (file)
index 0000000..523c7a5
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_backup_24.xml
new file mode 100644 (file)
index 0000000..1991891
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_drag_handle_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_drag_handle_24.xml
new file mode 100644 (file)
index 0000000..49e9a5e
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_help_24_white.xml b/app/src/main/res/drawable-anydpi/ic_baseline_help_24_white.xml
new file mode 100644 (file)
index 0000000..c77419a
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_help_outline_24_primary.xml b/app/src/main/res/drawable-anydpi/ic_baseline_help_outline_24_primary.xml
new file mode 100644 (file)
index 0000000..e67a7ec
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_import_export_24.xml
new file mode 100644 (file)
index 0000000..04dc3a5
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_qr_code_scanner_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_qr_code_scanner_24.xml
new file mode 100644 (file)
index 0000000..7c32f10
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml b/app/src/main/res/drawable-anydpi/ic_baseline_restore_24.xml
new file mode 100644 (file)
index 0000000..b006f61
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable-anydpi/ic_clear_accent_24dp.xml b/app/src/main/res/drawable-anydpi/ic_clear_accent_24dp.xml
new file mode 100644 (file)
index 0000000..a1cae6e
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_comment_gray_24dp.xml b/app/src/main/res/drawable-anydpi/ic_comment_gray_24dp.xml
new file mode 100644 (file)
index 0000000..bea11d0
--- /dev/null
@@ -0,0 +1,29 @@
+<!--
+  ~ 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="#CCCCCC"
+    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>
diff --git a/app/src/main/res/drawable-anydpi/ic_delete_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_delete_white_24dp.xml
new file mode 100644 (file)
index 0000000..7b5ced6
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ~ 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/colorOnPrimary"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    >
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
+        />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_error_outline_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_error_outline_black_24dp.xml
new file mode 100644 (file)
index 0000000..d8a883c
--- /dev/null
@@ -0,0 +1,31 @@
+<!--
+  ~ 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:autoMirrored="true"
+    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="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"
+        />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_event_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_event_black_24dp.xml
new file mode 100644 (file)
index 0000000..8a6db02
--- /dev/null
@@ -0,0 +1,24 @@
+<!--
+  ~ 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="?colorOnPrimary"
+    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>
diff --git a/app/src/main/res/drawable-anydpi/ic_event_gray_24dp.xml b/app/src/main/res/drawable-anydpi/ic_event_gray_24dp.xml
new file mode 100644 (file)
index 0000000..4bda6d5
--- /dev/null
@@ -0,0 +1,24 @@
+<!--
+  ~ 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="#FF606060"
+    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>
diff --git a/app/src/main/res/drawable-anydpi/ic_event_note_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_event_note_black_24dp.xml
new file mode 100644 (file)
index 0000000..59f4e89
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M17,10L7,10v2h10v-2zM19,3h-1L18,1h-2v2L8,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,-2zM19,19L5,19L5,8h14v11zM14,14L7,14v2h7v-2z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_expand_less_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_expand_less_black_24dp.xml
new file mode 100644 (file)
index 0000000..b739c6f
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ~ 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,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"
+        />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_filter_list_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_filter_list_black_24dp.xml
new file mode 100644 (file)
index 0000000..9eeb1cd
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="?colorPrimary"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_filter_list_white_24dp.xml
new file mode 100644 (file)
index 0000000..b864b8a
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ~ 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="?colorOnPrimary"
+    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="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"
+        />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_home_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_home_black_24dp.xml
new file mode 100644 (file)
index 0000000..77cb545
--- /dev/null
@@ -0,0 +1,29 @@
+<!--
+  ~ 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="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_mode_edit_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_mode_edit_black_24dp.xml
new file mode 100644 (file)
index 0000000..dd133ca
--- /dev/null
@@ -0,0 +1,30 @@
+<!--
+  ~ 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="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
+        />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_palette_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_palette_black_24dp.xml
new file mode 100644 (file)
index 0000000..6fa73bd
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="?colorPrimary"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_refresh_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_refresh_white_24dp.xml
new file mode 100644 (file)
index 0000000..94377a1
--- /dev/null
@@ -0,0 +1,24 @@
+<!--
+  ~ 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="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>
diff --git a/app/src/main/res/drawable-anydpi/ic_save_white_24dp.xml b/app/src/main/res/drawable-anydpi/ic_save_white_24dp.xml
new file mode 100644 (file)
index 0000000..7429776
--- /dev/null
@@ -0,0 +1,23 @@
+<!--
+  ~ 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="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/ic_settings_black_24dp.xml b/app/src/main/res/drawable-anydpi/ic_settings_black_24dp.xml
new file mode 100644 (file)
index 0000000..5e7da9c
--- /dev/null
@@ -0,0 +1,28 @@
+<!--
+  ~ 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="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z" />
+</vector>
diff --git a/app/src/main/res/drawable-anydpi/list_divider.xml b/app/src/main/res/drawable-anydpi/list_divider.xml
new file mode 100644 (file)
index 0000000..781efe8
--- /dev/null
@@ -0,0 +1,26 @@
+<?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="?colorPrimaryTransparent"
+        android:centerColor="?colorPrimary"
+        android:startColor="?colorPrimaryTransparent" />
+    <size android:height="2dp" />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-anydpi/side_nav_bar.xml b/app/src/main/res/drawable-anydpi/side_nav_bar.xml
new file mode 100644 (file)
index 0000000..2e34f12
--- /dev/null
@@ -0,0 +1,23 @@
+<!--
+  ~ 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="rectangle">
+    <solid
+        android:color="?colorPrimary"
+        />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/app_icon.png b/app/src/main/res/drawable-hdpi/app_icon.png
deleted file mode 100644 (file)
index 7df4fa1..0000000
Binary files a/app/src/main/res/drawable-hdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-ldpi/app_icon.png b/app/src/main/res/drawable-ldpi/app_icon.png
deleted file mode 100644 (file)
index ed0d78c..0000000
Binary files a/app/src/main/res/drawable-ldpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-mdpi/app_icon.png b/app/src/main/res/drawable-mdpi/app_icon.png
deleted file mode 100644 (file)
index 4e88a67..0000000
Binary files a/app/src/main/res/drawable-mdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-tvdpi/app_icon.png b/app/src/main/res/drawable-tvdpi/app_icon.png
deleted file mode 100644 (file)
index 8e8ed13..0000000
Binary files a/app/src/main/res/drawable-tvdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/app_icon.png b/app/src/main/res/drawable-xhdpi/app_icon.png
deleted file mode 100644 (file)
index a62a8df..0000000
Binary files a/app/src/main/res/drawable-xhdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxhdpi/app_icon.png b/app/src/main/res/drawable-xxhdpi/app_icon.png
deleted file mode 100644 (file)
index 41fe6c4..0000000
Binary files a/app/src/main/res/drawable-xxhdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xxxhdpi/app_icon.png b/app/src/main/res/drawable-xxxhdpi/app_icon.png
deleted file mode 100644 (file)
index 1a12e02..0000000
Binary files a/app/src/main/res/drawable-xxxhdpi/app_icon.png and /dev/null differ
diff --git a/app/src/main/res/drawable/app_icon_transparent_bg.xml b/app/src/main/res/drawable/app_icon_transparent_bg.xml
new file mode 100644 (file)
index 0000000..fc1f305
--- /dev/null
@@ -0,0 +1,51 @@
+<!--
+  ~ 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 android:autoMirrored="true" android:height="108dp"
+    android:viewportHeight="108" android:viewportWidth="108"
+    android:width="108dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="nonZero"
+        android:pathData="m54,30.731c-3.912,0 -7.824,1.483 -10.791,4.45a3.079,3.079 0,0 0,0.002 4.356,3.079 3.079,0 0,0 4.354,-0.002c3.58,-3.581 9.291,-3.581 12.871,0a3.079,3.079 0,0 0,4.354 0.002,3.079 3.079,0 0,0 0.002,-4.356C61.825,32.214 57.912,30.731 54,30.731Z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="6.5"/>
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="nonZero"
+        android:pathData="m54,19.05c-7.029,0 -14.057,2.673 -19.403,8.019a3.079,3.079 0,0 0,0 4.354,3.079 3.079,0 0,0 4.354,0c8.337,-8.338 21.76,-8.338 30.097,0a3.079,3.079 0,0 0,4.354 0,3.079 3.079,0 0,0 0,-4.354C68.058,21.723 61.029,19.05 54,19.05Z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="6.5"/>
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="nonZero"
+        android:pathData="m21.032,43.661c-2.918,0.602 -3.032,2.816 -3.032,3.402v39.464c0,1.887 1.578,3.468 3.466,3.468 11.792,-1.987 18.035,-1.916 32.535,0.006 13.366,-1.845 23.124,-2.039 32.534,-0.006 1.888,0 3.466,-1.581 3.466,-3.468V47.062c0,-1.893 -1.639,-3.198 -3.286,-3.458 -8.577,-1.357 -12.054,-1.986 -25.014,-0.838 0.438,1.333 0.651,2.519 0.399,3.783 9.689,-1.036 17.521,-0.622 24.152,0.809l-0.041,38.785C75.832,84.276 69.163,84.319 55.895,85.982V53.417c-1.285,0.314 -2.595,0.3 -3.789,0V85.974C41.539,84.283 31.082,84.286 21.79,86.145V47.358c9.973,-1.262 14.572,-1.849 24.109,-0.827 -0.163,-1.302 0.019,-2.692 0.407,-3.781 -8.212,-1.016 -15.376,-0.527 -25.274,0.911z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="4"/>
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="evenOdd"
+        android:pathData="m34.816,55.816v9.522h-9.52v4.642h9.52v9.52h4.643v-9.52h9.522v-4.642h-9.522v-9.522z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="4.9"/>
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="evenOdd"
+        android:pathData="m58.837,65.338v4.642h23.684v-4.642z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="4.9"/>
+    <path android:fillAlpha="1" android:fillColor="#ffffff"
+        android:fillType="nonZero"
+        android:pathData="m54,42.395c1.594,0 3.079,1.487 3.079,3.079 0,1.592 -1.485,3.076 -3.079,3.076 -1.594,0 -3.079,-1.483 -3.079,-3.076 0,-1.592 1.485,-3.079 3.079,-3.079z"
+        android:strokeAlpha="1" android:strokeColor="#00000000"
+        android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="10.4"/>
+</vector>
diff --git a/app/src/main/res/drawable/launcher_foreground.xml b/app/src/main/res/drawable/launcher_foreground.xml
new file mode 100644 (file)
index 0000000..af159e2
--- /dev/null
@@ -0,0 +1,79 @@
+<!--
+  ~ 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>
diff --git a/app/src/main/res/drawable/thick_plus_icon.xml b/app/src/main/res/drawable/thick_plus_icon.xml
new file mode 100644 (file)
index 0000000..c7a14de
--- /dev/null
@@ -0,0 +1,51 @@
+<!--
+  ~ 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="100dp"
+    android:height="100dp"
+    android:autoMirrored="true"
+    android:viewportWidth="100"
+    android:viewportHeight="100"
+    >
+    <path
+        android:fillColor="#ededed"
+        android:pathData="M50,50m-35,0a35,35 0,1 1,70 0a35,35 0,1 1,-70 0"
+        android:strokeWidth="0.370416"
+        android:strokeColor="#00000000"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="bevel"
+        />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="M50.19,35V65"
+        android:strokeWidth="9"
+        android:strokeColor="#935ff2"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="miter"
+        />
+    <path
+        android:fillColor="#00000000"
+        android:fillType="evenOdd"
+        android:pathData="M35,50L65,50"
+        android:strokeWidth="9"
+        android:strokeColor="#935ff2"
+        android:strokeLineCap="round"
+        android:strokeLineJoin="miter"
+        />
+</vector>
diff --git a/app/src/main/res/layout-w900dp/profile_list.xml b/app/src/main/res/layout-w900dp/profile_list.xml
deleted file mode 100644 (file)
index 3701f3a..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?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/>.
-  -->
-
-<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="match_parent"
-    android:layout_marginLeft="16dp"
-    android:layout_marginRight="16dp"
-    android:baselineAligned="false"
-    android:divider="?android:attr/dividerHorizontal"
-    android:orientation="horizontal"
-    android:showDividers="middle"
-    tools:context=".ui.activity.ProfileListActivity">
-
-    <!--
-    This layout is a two-pane layout for the Profiles
-    master/detail flow.
-    
-    -->
-
-    <androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:tools="http://schemas.android.com/tools"
-        android:id="@+id/profile_list"
-        android:name="net.ktnx.mobileledger.ui.activity.ProfileListFragment"
-        android:layout_width="@dimen/item_width"
-        android:layout_height="match_parent"
-        android:layout_marginLeft="16dp"
-        android:layout_marginRight="16dp"
-        app:layoutManager="LinearLayoutManager"
-        tools:context="net.ktnx.mobileledger.ui.activity.ProfileListActivity"
-        tools:listitem="@layout/profile_list_content" />
-
-    <FrameLayout
-        android:id="@+id/profile_detail_container"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        android:layout_weight="3" />
-
-</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/account_autocomplete_row.xml b/app/src/main/res/layout/account_autocomplete_row.xml
new file mode 100644 (file)
index 0000000..f30c706
--- /dev/null
@@ -0,0 +1,56 @@
+<?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
diff --git a/app/src/main/res/layout/account_list_row.xml b/app/src/main/res/layout/account_list_row.xml
new file mode 100644 (file)
index 0000000..185faf8
--- /dev/null
@@ -0,0 +1,122 @@
+<?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
diff --git a/app/src/main/res/layout/account_list_summary_row.xml b/app/src/main/res/layout/account_list_summary_row.xml
new file mode 100644 (file)
index 0000000..9dccb33
--- /dev/null
@@ -0,0 +1,46 @@
+<?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
index 449d4d2d17a4df037db501b1714e381f993f8a66..55f4af9f25322e50b1144b828d3ff0a2625669ba 100644 (file)
     android:id="@+id/account_summary_frame"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:id="@+id/account_summary_frame"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".ui.account_summary.AccountSummaryFragment">
+    tools:context=".ui.account_summary.AccountSummaryFragment"
+    >
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content_account_summary_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         app:layout_behavior="@string/appbar_scrolling_view_behavior"
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/content_account_summary_layout"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         app:layout_behavior="@string/appbar_scrolling_view_behavior"
-        tools:context=".ui.activity.MainActivity">
+        tools:context=".ui.activity.MainActivity"
+        >
 
         <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 
         <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
-            android:id="@+id/account_swiper"
+            android:id="@+id/account_swipe_refresh_layout"
             android:layout_width="match_parent"
             android:layout_width="match_parent"
-            android:layout_height="match_parent">
+            android:layout_height="match_parent"
+            >
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/account_root"
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/account_root"
@@ -42,7 +45,8 @@
                 android:choiceMode="multipleChoice"
                 android:drawSelectorOnTop="true"
                 android:orientation="vertical"
                 android:choiceMode="multipleChoice"
                 android:drawSelectorOnTop="true"
                 android:orientation="vertical"
-                android:scrollbars="vertical">
+                android:scrollbars="vertical"
+                >
 
             </androidx.recyclerview.widget.RecyclerView>
         </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
 
             </androidx.recyclerview.widget.RecyclerView>
         </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
diff --git a/app/src/main/res/layout/account_summary_row.xml b/app/src/main/res/layout/account_summary_row.xml
deleted file mode 100644 (file)
index ca88fe8..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-<?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"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:id="@+id/account_summary_row"
-    android:layout_width="match_parent"
-    android:longClickable="true"
-    android:layout_height="wrap_content">
-
-    <CheckBox
-        android:id="@+id/account_row_check"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:button="@drawable/checkbox_star_black"
-        android:onClick="onAccountSummaryRowViewClicked"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <TextView
-        android:id="@+id/account_row_acc_name"
-        style="@style/account_summary_account_name"
-        android:layout_width="wrap_content"
-        android:layout_height="0dp"
-        android:layout_weight="1"
-        android:gravity="center_vertical"
-        android:onClick="onAccountSummaryRowViewClicked"
-        android:longClickable="true"
-        android:paddingStart="8dp"
-        android:text="Account name, a really long one. A very very very long one. It may even spawn on more than two lines -- three, four or more."
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toEndOf="@id/account_row_check"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="HardcodedText" />
-
-    <FrameLayout
-        android:id="@+id/account_expander_container"
-        android:layout_width="@dimen/thumb_row_height"
-        android:layout_height="@dimen/thumb_row_height"
-        android:foregroundGravity="center_vertical"
-        android:minHeight="@dimen/thumb_row_height"
-        android:onClick="onAccountSummaryRowViewClicked"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintStart_toEndOf="@id/account_row_acc_name"
-        app:layout_constraintTop_toTopOf="parent">
-
-        <ImageView
-            android:id="@+id/account_expander"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_margin="8dp"
-            android:background="@drawable/ic_expand_less_black_24dp"
-            android:clickable="true"
-            android:onClick="onAccountSummaryRowViewClicked" />
-    </FrameLayout>
-
-    <TextView
-        android:id="@+id/account_row_filler1"
-        android:layout_width="0dp"
-        android:layout_height="match_parent"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/account_row_acc_amounts"
-        app:layout_constraintStart_toEndOf="@id/account_expander_container"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <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_marginEnd="8dp"
-        android:layout_weight="0"
-        android:gravity="center_vertical"
-        android:minWidth="@dimen/thumb_row_height"
-        android:onClick="onAccountSummaryRowViewClicked"
-        android:text="123,45\n678,90"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        tools:ignore="HardcodedText" />
-
-    <FrameLayout
-        android:id="@+id/account_row_amounts_expander_container"
-        android:layout_width="0dp"
-        android:layout_height="12sp"
-        app:layout_constraintStart_toStartOf="@id/account_row_acc_amounts"
-        app:layout_constraintEnd_toEndOf="@id/account_row_acc_amounts"
-        app:layout_constraintBottom_toBottomOf="@id/account_row_acc_amounts"
-        android:background="@drawable/fade_down_white">
-
-        <!--<ImageView-->
-            <!--android:layout_gravity="center_vertical|end"-->
-            <!--android:id="@+id/account_row_amounts_expander"-->
-            <!--android:layout_width="20dp"-->
-            <!--android:layout_height="20dp"-->
-            <!--android:background="@drawable/ic_expand_more_black_24dp" />-->
-
-    </FrameLayout>
-
-    <View
-        android:id="@+id/account_summary_trailer"
-        android:layout_width="match_parent"
-        android:layout_height="80dp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        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
index 61592e84179582ccd098b586877a2fff38862271..bd74773af3cbd3f81aad3aaac663e7e9f0980024 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
   ~ You should have received a copy of the GNU General Public License
   ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
   -->
   ~ 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"
+<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"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:theme="@style/AppTheme.AppBarOverlay"
-    tools:context=".ui.activity.MainActivity">
+    tools:context=".ui.activity.MainActivity"
+    >
 
 
-    <androidx.appcompat.widget.Toolbar
-        android:id="@+id/toolbar"
+    <ScrollView
+        android:id="@+id/no_profiles_layout"
         android:layout_width="match_parent"
         android:layout_width="match_parent"
-        android:layout_height="?attr/actionBarSize"
-        android:background="?colorPrimary"
-        app:popupTheme="@style/AppTheme.PopupOverlay" />
+        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"
+            >
+
+            <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"
+                >
 
 
-    <androidx.drawerlayout.widget.DrawerLayout
-        android:id="@+id/drawer_layout"
+                <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:layout_width="match_parent"
         android:layout_height="match_parent"
-        tools:openDrawer="start">
+        android:background="?android:attr/colorBackground"
+        android:orientation="vertical"
+        android:visibility="gone"
+        >
 
 
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
+        <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"
+            />
 
 
-            <include layout="@layout/loading" />
+        <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
 
             <androidx.constraintlayout.widget.ConstraintLayout
-                android:visibility="gone"
                 android:id="@+id/pager_layout"
                 android:layout_width="match_parent"
                 android:id="@+id/pager_layout"
                 android:layout_width="match_parent"
-                android:layout_height="match_parent">
+                android:layout_height="match_parent"
+                >
 
 
-                <com.google.android.material.floatingactionbutton.FloatingActionButton
-                    android:id="@+id/btn_add_transaction"
-                    android:layout_width="wrap_content"
+                <androidx.appcompat.widget.Toolbar
+                    android:id="@+id/toolbar"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_gravity="bottom|end"
-                    android:layout_margin="@dimen/fab_margin"
-                    app:backgroundTint="?colorAccent"
-                    app:layout_constraintBottom_toBottomOf="parent"
+                    android:background="?colorPrimary"
+                    android:theme="@style/AppTheme.AppBarOverlay"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintEnd_toEndOf="parent"
-                    app:maxImageSize="36dp"
-                    app:srcCompat="@drawable/ic_add_white_24dp" />
+                    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
 
                 <LinearLayout
-                    android:id="@+id/main_header"
+                    android:id="@+id/transaction_progress_layout"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:orientation="vertical"
+                    android:gravity="center_vertical"
+                    android:orientation="horizontal"
+                    android:visibility="gone"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="parent">
+                    app:layout_constraintTop_toBottomOf="@id/toolbar"
+                    >
 
 
-                    <LinearLayout
-                        android:id="@+id/transactions_last_update_layout"
-                        android:layout_width="match_parent"
+                    <ProgressBar
+                        android:id="@+id/transaction_list_progress_bar"
+                        style="?android:attr/progressBarStyleHorizontal"
+                        android:layout_width="0dp"
                         android:layout_height="wrap_content"
                         android:layout_height="wrap_content"
-                        android:elevation="24dp"
-                        android:orientation="horizontal">
+                        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_last_update_label"
-                            android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:paddingStart="8dp"
-                            android:paddingEnd="8dp"
-                            android:text="@string/transactions_last_update_label"
-                            android:textColor="@android:color/tertiary_text_light" />
-
-                        <TextView
-                            android:id="@+id/transactions_last_update"
-                            style="@android:style/Widget.DeviceDefault.Light.TextView"
-                            android:layout_width="0dp"
-                            android:layout_height="wrap_content"
-                            android:layout_weight="1"
-                            android:text="\?"
-                            android:textColor="@android:color/tertiary_text_light"
-                            tools:ignore="HardcodedText" />
-                    </LinearLayout>
-
-                    <LinearLayout
-                        android:id="@+id/transaction_progress_layout"
-                        android:layout_width="match_parent"
+                    <TextView
+                        android:id="@+id/transaction_list_cancel_download"
+                        android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         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: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_black_24dp"
-                            android:clickable="true"
-                            android:focusable="true"
-                            android:onClick="onStopTransactionRefreshClick" />
-                    </LinearLayout>
-
+                        android:background="@drawable/ic_clear_accent_24dp"
+                        android:clickable="true"
+                        android:focusable="true"
+                        />
                 </LinearLayout>
 
                 </LinearLayout>
 
-                <androidx.viewpager.widget.ViewPager
-                    android:id="@+id/root_frame"
+                <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"
                     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">
+                    app:layout_constraintTop_toBottomOf="@id/transaction_progress_layout"
+                    >
 
 
-                </androidx.viewpager.widget.ViewPager>
+                </androidx.viewpager2.widget.ViewPager2>
 
                 <View
                     android:layout_width="0dp"
 
                 <View
                     android:layout_width="0dp"
-                    android:layout_height="4dp"
+                    android:layout_height="?attr/main_header_shadow_height"
                     android:background="@drawable/drop_shadow"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
                     android:background="@drawable/drop_shadow"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/main_header" />
+                    app:layout_constraintTop_toBottomOf="@id/transaction_progress_layout"
+                    />
 
 
             </androidx.constraintlayout.widget.ConstraintLayout>
 
 
 
             </androidx.constraintlayout.widget.ConstraintLayout>
 
-            <include layout="@layout/no_profiles" />
-        </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>
 
 
-        <include layout="@layout/main_navigation" />
+                </androidx.constraintlayout.widget.ConstraintLayout>
 
 
-    </androidx.drawerlayout.widget.DrawerLayout>
-</LinearLayout>
+            </com.google.android.material.navigation.NavigationView>
+        </androidx.drawerlayout.widget.DrawerLayout>
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
+</FrameLayout>
\ No newline at end of file
index 3ff1415afdac1e9e682c0154017568815bcd2c71..55273c7414e947136fe40be282f45fe0444a045c 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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"
     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
         android:layout_width="match_parent"
         android:layout_height="match_parent"
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:animateLayoutChanges="true">
+        android:animateLayoutChanges="true"
+        >
 
         <TextView
             android:id="@+id/simulationLabel"
 
         <TextView
             android:id="@+id/simulationLabel"
-            style="@style/StretchedTextView"
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:layout_margin="@dimen/activity_horizontal_margin"
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:layout_margin="@dimen/activity_horizontal_margin"
             android:rotation="-45"
             android:text="@string/simulation_label"
             android:textAlignment="center"
             android:rotation="-45"
             android:text="@string/simulation_label"
             android:textAlignment="center"
-            android:textColor="@color/table_row_dark_bg"
+            android:textColor="?attr/table_row_dark_bg"
             android:textIsSelectable="false"
             android:textSize="36sp"
             android:textStyle="bold"
             android:textIsSelectable="false"
             android:textSize="36sp"
             android:textStyle="bold"
+            android:visibility="gone"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/toolbar_layout" />
+            app:layout_constraintTop_toBottomOf="@+id/toolbar_layout"
+            />
 
         <com.google.android.material.appbar.AppBarLayout
             android:id="@+id/toolbar_layout"
 
         <com.google.android.material.appbar.AppBarLayout
             android:id="@+id/toolbar_layout"
             android:theme="@style/AppTheme.AppBarOverlay"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             android:theme="@style/AppTheme.AppBarOverlay"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent">
+            app:layout_constraintTop_toTopOf="parent"
+            >
 
             <androidx.appcompat.widget.Toolbar
                 android:id="@+id/toolbar"
                 android:layout_width="match_parent"
 
             <androidx.appcompat.widget.Toolbar
                 android:id="@+id/toolbar"
                 android:layout_width="match_parent"
-                android:layout_height="?attr/actionBarSize"
+                android:layout_height="wrap_content"
+                android:minHeight="?attr/actionBarSize"
                 android:background="?attr/colorPrimary"
                 android:background="?attr/colorPrimary"
-                app:popupTheme="@style/AppTheme.PopupOverlay" />
+                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>
 
 
         </com.google.android.material.appbar.AppBarLayout>
 
-        <fragment
+        <androidx.fragment.app.FragmentContainerView
             android:id="@+id/new_transaction_nav"
             android:name="androidx.navigation.fragment.NavHostFragment"
             android:layout_width="0dp"
             android:layout_height="0dp"
             android:id="@+id/new_transaction_nav"
             android:name="androidx.navigation.fragment.NavHostFragment"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            app:defaultNavHost="true"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/toolbar_layout"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/toolbar_layout"
-            app:navGraph="@navigation/new_transaction_navigation" />
+            app:navGraph="@navigation/new_transaction_navigation"
+            />
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
     </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
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
index 9213f52a5166eb4ff24bc193396823206be9c189..0a379e088744de8ff31346f44d993973cf7a80af 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 <?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
   ~ This file is part of MoLe.
   ~ 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"
     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"
 
     <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"
 
         <com.google.android.material.appbar.CollapsingToolbarLayout
             android:id="@+id/toolbar_layout"
@@ -44,7 +47,8 @@
                 android:layout_width="match_parent"
                 android:layout_height="?attr/actionBarSize"
                 app:layout_collapseMode="pin"
                 android:layout_width="match_parent"
                 android:layout_height="?attr/actionBarSize"
                 app:layout_collapseMode="pin"
-                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
+                app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
+                />
 
         </com.google.android.material.appbar.CollapsingToolbarLayout>
 
 
         </com.google.android.material.appbar.CollapsingToolbarLayout>
 
         app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
     <com.google.android.material.floatingactionbutton.FloatingActionButton
         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"
         android:layout_margin="@dimen/fab_margin"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_vertical|start"
         android:layout_margin="@dimen/fab_margin"
-        app:backgroundTint="?colorAccent"
+        app:backgroundTint="?colorSecondary"
         app:layout_anchor="@+id/profile_detail_container"
         app:layout_anchorGravity="top|end"
         app:srcCompat="@drawable/ic_save_white_24dp" />
         app:layout_anchor="@+id/profile_detail_container"
         app:layout_anchorGravity="top|end"
         app:srcCompat="@drawable/ic_save_white_24dp" />
diff --git a/app/src/main/res/layout/activity_templates.xml b/app/src/main/res/layout/activity_templates.xml
new file mode 100644 (file)
index 0000000..cd2b409
--- /dev/null
@@ -0,0 +1,68 @@
+<?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>
index 8c1658f6cdd271cbfc00de4864be8c9611fea620..bc5194e28675400453c51c368648677380b3f379 100644 (file)
@@ -1,6 +1,5 @@
-<?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
   ~ This file is part of MoLe.
   ~ 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/>.
   -->
 
   ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
-
-    <CalendarView
-        android:id="@+id/calendarView"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent" />
-</LinearLayout>
\ No newline at end of file
+<CalendarView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/calendarView"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    />
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_backups.xml b/app/src/main/res/layout/fragment_backups.xml
new file mode 100644 (file)
index 0000000..89344af
--- /dev/null
@@ -0,0 +1,130 @@
+<?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
diff --git a/app/src/main/res/layout/fragment_currency_selector.xml b/app/src/main/res/layout/fragment_currency_selector.xml
new file mode 100644 (file)
index 0000000..56309be
--- /dev/null
@@ -0,0 +1,37 @@
+<?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/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:longClickable="true">
+
+    <TextView
+        android:id="@+id/content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="@dimen/text_margin"
+        android:text="USD"
+        android:textAppearance="?attr/textAppearanceListItem"
+        tools:ignore="HardcodedText"
+        android:minWidth="20dp"
+        android:gravity="center_horizontal"
+        />
+</LinearLayout>
diff --git a/app/src/main/res/layout/fragment_currency_selector_list.xml b/app/src/main/res/layout/fragment_currency_selector_list.xml
new file mode 100644 (file)
index 0000000..a44f0ec
--- /dev/null
@@ -0,0 +1,178 @@
+<?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: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_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"
+        />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list"
+        android:name="net.ktnx.mobileledger.ui.CurrencySelectorFragment"
+        android:layout_width="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_constraintBottom_toTopOf="@id/new_currency_panel"
+        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_currency_selector"
+        >
+
+    </androidx.recyclerview.widget.RecyclerView>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/new_currency_panel"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="@dimen/activity_horizontal_margin"
+        android:layout_marginRight="@dimen/activity_horizontal_margin"
+        app:layout_constraintBottom_toTopOf="@id/params_panel"
+        app:layout_constraintEnd_toEndOf="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: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"
+            />
+        <TextView
+            android:id="@+id/btn_no_currency"
+            style="@style/Widget.MaterialComponents.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/text_margin"
+            android:text="@string/btn_no_currency"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toStartOf="@id/btn_add_new"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            />
+
+        <EditText
+            android:id="@+id/new_currency_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:hint="@string/new_currency_name_hint"
+            android:inputType="text"
+            android:singleLine="true"
+            android:text=""
+            android:visibility="invisible"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            android:autofillHints="currency"
+            />
+
+        <TextView
+            android:id="@+id/btn_add_currency"
+            style="@style/Widget.MaterialComponents.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/text_margin"
+            android:text="@string/add_button"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            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"
+        app:layout_constraintBottom_toBottomOf="parent"
+        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"
+            android:layout_marginBottom="@dimen/text_margin"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toTopOf="@id/currency_gap"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            >
+
+            <RadioButton
+                android:id="@+id/currency_position_left"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                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"
+                />
+        </RadioGroup>
+
+        <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_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/position_radio_group"
+            />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index b5ce2a8dd25fc0b211127788b761bd0a3921cbe6..6ddb6b10fb8e5f0568574317acad5be9916a6c62 100644 (file)
@@ -1,5 +1,5 @@
 <!--
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
   -->
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
   -->
 <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:layout_width="match_parent"
     android:layout_height="match_parent">
 
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <androidx.constraintlayout.widget.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
+    <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        tools:context="net.ktnx.mobileledger.ui.activity.NewTransactionActivity">
+        android:animateLayoutChanges="true"
+        tools:context="net.ktnx.mobileledger.ui.new_transaction.NewTransactionActivity"
+        >
+
+        <ProgressBar
+            android:id="@+id/progressBar"
+            style="@style/Widget.AppCompat.ProgressBar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            android:indeterminate="true"
+            android:indeterminateBehavior="cycle"
+            android:visibility="invisible"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/new_transaction_accounts"
             android:layout_width="0dp"
             android:layout_height="0dp"
 
         <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/new_transaction_accounts"
             android:layout_width="0dp"
             android:layout_height="0dp"
+            android:orientation="vertical"
             android:paddingStart="@dimen/activity_horizontal_margin"
             android:paddingEnd="@dimen/activity_horizontal_margin"
             app:layout_constraintBottom_toBottomOf="parent"
             android:paddingStart="@dimen/activity_horizontal_margin"
             android:paddingEnd="@dimen/activity_horizontal_margin"
             app:layout_constraintBottom_toBottomOf="parent"
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
 
     </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:color/white"
-        android:visibility="visible"
-        app:backgroundTint="?colorAccent"
-        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
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
index 886d90571aff13a6519be45a169267fd1a2c11c4..f086806d814521e6dda174a46096ed6ad0ce140d 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
 <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"
 <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="match_parent">
+    android:layout_height="match_parent"
+    >
 
 
 
 
-    <TextView
-        android:id="@+id/textView4"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:text="@string/new_transaction_saving"
-        android:textAlignment="center"
-        android:textSize="18sp"
-        app:layout_constraintBottom_toTopOf="@+id/progressBar"
+    <ImageView
+        android:id="@+id/imageView"
+        android:layout_width="180dp"
+        android:layout_height="180dp"
+        android:tint="?colorPrimary"
+        app:layout_constraintBottom_toTopOf="@id/progressBar"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent" />
-
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed"
+        app:srcCompat="@drawable/app_icon_transparent_bg"
+        android:contentDescription="@string/splash_icon_description"
+        />
     <ProgressBar
         android:id="@+id/progressBar"
         style="@style/Widget.AppCompat.ProgressBar.Horizontal"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:indeterminate="true"
     <ProgressBar
         android:id="@+id/progressBar"
         style="@style/Widget.AppCompat.ProgressBar.Horizontal"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:indeterminate="true"
-        android:progressTint="@color/colorAccent"
+        android:progressTint="?attr/colorPrimary"
+        app:layout_constraintBottom_toTopOf="@id/textView4"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/imageView"
+        />
+    <TextView
+        android:id="@+id/textView4"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/new_transaction_saving"
+        android:textAlignment="center"
+        android:textSize="18sp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toBottomOf="@id/progressBar"
+        />
+    <androidx.core.widget.ContentLoadingProgressBar
+        android:layout_width="60dp"
+        android:layout_height="60dp"
+        android:indeterminate="true"
+        android:indeterminateTint="?colorPrimary"
+        android:progressTint="?colorPrimary"
+        app:layout_constraintBottom_toBottomOf="@id/textView4"
+        app:layout_constraintEnd_toStartOf="@id/textView4"
+        app:layout_constraintTop_toTopOf="@id/textView4"
+        />
 
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
 
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_template_detail_source_selector.xml b/app/src/main/res/layout/fragment_template_detail_source_selector.xml
new file mode 100644 (file)
index 0000000..d3af166
--- /dev/null
@@ -0,0 +1,60 @@
+<?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>
diff --git a/app/src/main/res/layout/fragment_template_detail_source_selector_list.xml b/app/src/main/res/layout/fragment_template_detail_source_selector_list.xml
new file mode 100644 (file)
index 0000000..f21a43d
--- /dev/null
@@ -0,0 +1,78 @@
+<?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
diff --git a/app/src/main/res/layout/fragment_template_list.xml b/app/src/main/res/layout/fragment_template_list.xml
new file mode 100644 (file)
index 0000000..a81e9fb
--- /dev/null
@@ -0,0 +1,24 @@
+<?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"
+    />
diff --git a/app/src/main/res/layout/last_update_layout.xml b/app/src/main/res/layout/last_update_layout.xml
new file mode 100644 (file)
index 0000000..b1640f3
--- /dev/null
@@ -0,0 +1,42 @@
+<?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/last_update_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    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
diff --git a/app/src/main/res/layout/loading.xml b/app/src/main/res/layout/loading.xml
deleted file mode 100644 (file)
index fe56eca..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<?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:id="@+id/loading_layout"
-    android:layout_width="match_parent"
-    android:layout_height="match_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_loading"
-        android:textColor="?colorPrimary"
-        android:textSize="48sp"
-        app:layout_constraintBottom_toBottomOf="parent"
-        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
diff --git a/app/src/main/res/layout/main_navigation.xml b/app/src/main/res/layout/main_navigation.xml
deleted file mode 100644 (file)
index 14c2ecc..0000000
+++ /dev/null
@@ -1,174 +0,0 @@
-<?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/>.
-  -->
-
-<com.google.android.material.navigation.NavigationView 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/nav_view"
-    android:layout_width="wrap_content"
-    android:layout_height="match_parent"
-    android:layout_gravity="start"
-    android:fitsSystemWindows="true"
-    android:theme="@style/ThemeOverlay.AppCompat.Light"
-    tools:showIn="@layout/activity_main">
-
-
-    <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:onClick="navSettingsClicked"
-                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:divider="@drawable/list_divider_inside_out"
-                android:orientation="vertical"
-                android:showDividers="beginning"
-                app:layout_constraintBottom_toTopOf="@+id/nav_fixed_items"
-                app:layout_constraintTop_toBottomOf="@+id/nav_header">
-
-                <LinearLayout
-                    android:id="@+id/nav_header"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:background="@drawable/side_nav_bar"
-                    android:gravity="center_vertical"
-                    android:orientation="horizontal"
-                    android:padding="@dimen/activity_vertical_margin"
-                    android:theme="@style/ThemeOverlay.AppCompat.Dark"
-                    app:layout_constraintTop_toTopOf="parent">
-
-                    <include layout="@layout/nav_header_logo" />
-
-                    <LinearLayout
-                        android:layout_width="match_parent"
-                        android:layout_height="match_parent"
-                        android:layout_marginStart="@dimen/activity_horizontal_margin"
-                        android:layout_marginEnd="@dimen/activity_horizontal_margin"
-                        android:gravity="center_vertical"
-                        android:orientation="vertical">
-
-                        <TextView
-                            android:layout_width="match_parent"
-                            android:layout_height="wrap_content"
-                            android:text="@string/app_name"
-                            android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
-
-                        <TextView
-                            android:id="@+id/drawer_version_text"
-                            android:layout_width="wrap_content"
-                            android:layout_height="wrap_content"
-                            android:text="dummy version"
-                            tools:ignore="HardcodedText" />
-                    </LinearLayout>
-
-                </LinearLayout>
-
-                <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"
-                            android:paddingStart="0dp">
-
-                        </androidx.recyclerview.widget.RecyclerView>
-
-                    </LinearLayout>
-
-                </LinearLayout>
-
-            </LinearLayout>
-        </ScrollView>
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-
-</com.google.android.material.navigation.NavigationView>
\ No newline at end of file
diff --git a/app/src/main/res/layout/nav_header_layout.xml b/app/src/main/res/layout/nav_header_layout.xml
new file mode 100644 (file)
index 0000000..7549fdf
--- /dev/null
@@ -0,0 +1,59 @@
+<?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:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/nav_header"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@drawable/side_nav_bar"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    android:padding="@dimen/activity_vertical_margin"
+    app:layout_constraintTop_toTopOf="parent"
+    >
+
+    <include layout="@layout/nav_header_logo" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginStart="@dimen/activity_horizontal_margin"
+        android:layout_marginEnd="@dimen/activity_horizontal_margin"
+        android:gravity="center_vertical"
+        android:orientation="vertical"
+        >
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/app_name"
+            android:textColor="?colorOnPrimary"
+            android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
+
+        <TextView
+            android:id="@+id/drawer_version_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="dummy version"
+            android:textColor="?attr/colorOnPrimary"
+            tools:ignore="HardcodedText" />
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
index e91c7b4e11a339ede8e1226ce2d8b6222188b016..a4dd89bbb49fb28742fe0bfcc2697f3052d687c0 100644 (file)
@@ -23,5 +23,5 @@
     android:contentDescription="@string/nav_header_desc"
     android:visibility="visible"
     android:id="@+id/app_icon"
     android:contentDescription="@string/nav_header_desc"
     android:visibility="visible"
     android:id="@+id/app_icon"
-    app:srcCompat="@drawable/app_icon_dynamic"
+    app:srcCompat="@drawable/app_icon_transparent_bg"
     tools:showIn="@layout/activity_main" />
\ No newline at end of file
     tools:showIn="@layout/activity_main" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/nav_profile_list_head.xml b/app/src/main/res/layout/nav_profile_list_head.xml
deleted file mode 100644 (file)
index 9755033..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-<?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" />
-
-    <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_black_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" />
-
-        <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" />
-
-    </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
diff --git a/app/src/main/res/layout/new_transaction_account_row.xml b/app/src/main/res/layout/new_transaction_account_row.xml
new file mode 100644 (file)
index 0000000..0092a0e
--- /dev/null
@@ -0,0 +1,136 @@
+<?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
diff --git a/app/src/main/res/layout/new_transaction_header_row.xml b/app/src/main/res/layout/new_transaction_header_row.xml
new file mode 100644 (file)
index 0000000..9f0ff19
--- /dev/null
@@ -0,0 +1,110 @@
+<?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
diff --git a/app/src/main/res/layout/new_transaction_row.xml b/app/src/main/res/layout/new_transaction_row.xml
deleted file mode 100644 (file)
index 885adbe..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-<?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/>.
-  -->
-
-<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="false"
-    android:orientation="vertical">
-
-    <LinearLayout
-        android:id="@+id/ntr_data"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <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:ems="10"
-            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_constraintBottom_toBottomOf="parent"
-            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="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_gravity="bottom"
-            android:layout_marginStart="8dp"
-            android:accessibilityTraversalAfter="@+id/new_transaction_date"
-            android:ems="10"
-            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_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_weight="30"
-            app:layout_constraintStart_toEndOf="@+id/new_transaction_date"
-            app:layout_constraintTop_toTopOf="parent" />
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/ntr_account"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
-
-
-        <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" />
-
-        <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="numberSigned|numberDecimal|number"
-            android:minWidth="70sp"
-            android:selectAllOnFocus="true"
-            android:textAlignment="viewEnd" />
-
-    </LinearLayout>
-
-    <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
diff --git a/app/src/main/res/layout/no_profiles.xml b/app/src/main/res/layout/no_profiles.xml
deleted file mode 100644 (file)
index f3a4d4d..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?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:id="@+id/no_profiles_layout"
-    android:visibility="gone"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="?table_row_dark_bg"
-    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="?colorAccent"
-        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>
\ No newline at end of file
index d095f094ae91876be8fc324b918f55827ac6e4e7..f2218a3a789f99e93a0c2d9f108fdcc4ba73b5eb 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ 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:orientation="vertical"
     android:padding="16dp"
     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"
 
     <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"
 
         <com.google.android.material.textfield.TextInputEditText
             android:id="@+id/profile_name"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:ems="10"
             android:hint="@string/profile_name_label"
             android:hint="@string/profile_name_label"
-            android:inputType="textPersonName" />
+            android:inputType="textPersonName"
+            />
     </com.google.android.material.textfield.TextInputLayout>
 
     <com.google.android.material.textfield.TextInputLayout
     </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: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_width="match_parent"
             android:layout_height="wrap_content"
 
         <com.google.android.material.textfield.TextInputEditText
             android:id="@+id/url"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:ems="10"
             android:hint="@string/url_label"
             android:inputType="textUri"
             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"
     </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: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"
 
         <LinearLayout
             android:id="@+id/auth_params"
             android:animateLayoutChanges="true"
             android:orientation="vertical"
             android:paddingStart="8dp"
             android:animateLayoutChanges="true"
             android:orientation="vertical"
             android:paddingStart="8dp"
-            tools:ignore="RtlSymmetry">
+            tools:ignore="RtlSymmetry"
+            >
 
             <LinearLayout
                 android:id="@+id/insecure_scheme_text"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginBottom="@dimen/activity_vertical_margin"
 
             <LinearLayout
                 android:id="@+id/insecure_scheme_text"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginBottom="@dimen/activity_vertical_margin"
-                android:background="@color/error_background"
+                android:background="?colorError"
                 android:padding="@dimen/activity_vertical_margin"
                 android:padding="@dimen/activity_vertical_margin"
-                android:visibility="gone">
+                android:visibility="gone"
+                >
 
                 <TextView
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
 
                 <TextView
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:text="@string/insecure_scheme_with_auth" />
+                    android:text="@string/insecure_scheme_with_auth"
+                    android:textColor="?colorOnError"
+                    />
             </LinearLayout>
 
             <com.google.android.material.textfield.TextInputLayout
             </LinearLayout>
 
             <com.google.android.material.textfield.TextInputLayout
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginBottom="16dp"
                 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"
 
                 <com.google.android.material.textfield.TextInputEditText
                     android:id="@+id/auth_user_name"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:ems="10"
                     android:hint="@string/pref_title_backend_auth_user"
                     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
             </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"
                 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"
 
                 <com.google.android.material.textfield.TextInputEditText
                     android:id="@+id/password"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:ems="10"
                     android:hint="@string/pref_title_backend_auth_password"
                     android:hint="@string/pref_title_backend_auth_password"
-                    android:inputType="textWebPassword" />
+                    android:inputType="textWebPassword"
+                    />
 
             </com.google.android.material.textfield.TextInputLayout>
 
         </LinearLayout>
 
 
             </com.google.android.material.textfield.TextInputLayout>
 
         </LinearLayout>
 
-        <Switch
-            android:id="@+id/profile_permit_posting"
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/server_version_layout"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="16dp"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="16dp"
-            android:text="@string/posting_permitted"
-            android:textAppearance="?android:textAppearanceListItem" />
+            >
 
 
-        <LinearLayout
-            android:id="@+id/future_dates_layout"
+            <TextView
+                android:id="@+id/server_version_label"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                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/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"
+                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/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"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="16dp"
-            android:orientation="vertical">
+            >
 
             <TextView
 
             <TextView
-                android:id="@+id/future_dates_title"
-                android:layout_width="match_parent"
+                android:id="@+id/api_version_label"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="@string/profile_future_dates_label"
-                android:textAppearance="?android:textAppearanceListItem" />
+                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_toTopOf="parent"
+                />
 
             <TextView
 
             <TextView
-                android:id="@+id/future_dates_text"
-                android:layout_width="match_parent"
+                android:id="@+id/api_version_text"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
+                android:layout_marginEnd="24dp"
                 android:textAppearance="?android:textAppearanceListItemSecondary"
                 android:textAppearance="?android:textAppearanceListItemSecondary"
-                android:textColor="?attr/textColor" />
-        </LinearLayout>
+                android:textColor="?attr/textColor"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/api_version_label"
+                />
+        </androidx.constraintlayout.widget.ConstraintLayout>
 
 
-        <com.google.android.material.textfield.TextInputLayout
-            android:id="@+id/preferred_accounts_accounts_filter_layout"
+        <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:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="16dp"
-            android:orientation="vertical">
+            android:text="@string/posting_permitted"
+            android:textAppearance="?android:textAppearanceListItem"
+            />
 
 
-            <com.google.android.material.textfield.TextInputEditText
-                android:id="@+id/preferred_accounts_filter_filter"
+        <LinearLayout
+            android:id="@+id/posting_sub_items"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            >
+
+            <LinearLayout
+                android:id="@+id/default_commodity_layout"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:ems="10"
-                android:fontFamily="monospace"
-                android:hint="@string/pref_preferred_autocompletion_account_filter_hint"
-                android:inputType="text"
-                android:textColor="?attr/editTextColor" />
-        </com.google.android.material.textfield.TextInputLayout>
+                android:layout_marginBottom="16dp"
+                android:clickable="true"
+                android:focusable="true"
+                android:orientation="vertical"
+                >
+
+                <TextView
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/profile_default_commodity"
+                    android:textAppearance="?android:textAppearanceListItem"
+                    />
+
+                <TextView
+                    android:id="@+id/default_commodity_text"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/btn_no_currency"
+                    android:textAppearance="?android:textAppearanceListItemSecondary"
+                    android:textColor="?attr/textColor"
+                    />
+            </LinearLayout>
+
+            <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"
+                />
+
+            <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"
+                />
+
+            <LinearLayout
+                android:id="@+id/future_dates_layout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="16dp"
+                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"
+                    />
+
+                <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"
+                    />
+            </LinearLayout>
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/preferred_accounts_layout"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="16dp"
+                android:orientation="vertical"
+                >
+
+                <com.google.android.material.textfield.TextInputEditText
+                    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"
+                    />
+            </com.google.android.material.textfield.TextInputLayout>
+        </LinearLayout>
 
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
 
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:orientation="horizontal">
+            android:orientation="horizontal"
+            >
 
             <TextView
                 android:layout_width="wrap_content"
 
             <TextView
                 android:layout_width="wrap_content"
                 android:layout_weight="100"
                 android:gravity="center_vertical"
                 android:text="@string/profile_color_label"
                 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"
 
             <ImageButton
                 android:id="@+id/btn_pick_ring_color"
                 android:layout_weight="1"
                 android:background="?colorPrimary"
                 android:contentDescription="@string/btn_color_picker_button"
                 android:layout_weight="1"
                 android:background="?colorPrimary"
                 android:contentDescription="@string/btn_color_picker_button"
-                android:src="@drawable/ic_palette_black_24dp"
-                android:tint="?drawer_background" />
+                android:tint="?colorOnPrimarySurface"
+                app:srcCompat="@drawable/ic_palette_black_24dp"
+                />
 
         </LinearLayout>
 
 
         </LinearLayout>
 
diff --git a/app/src/main/res/layout/profile_list.xml b/app/src/main/res/layout/profile_list.xml
deleted file mode 100644 (file)
index ef1d668..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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.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:id="@+id/profile_list"
-    android:name="net.ktnx.mobileledger.ui.activity.ProfileListFragment"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    app:layoutManager="LinearLayoutManager"
-    tools:context=".ui.activity.ProfileListActivity"
-    tools:listitem="@layout/profile_list_content" />
\ No newline at end of file
index 19b77e5d06ebcc7fad873c82bc9fda66cbf11b4e..2f0d6797ad745c0503643d67a7a62d4dce38d709 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 <?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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -40,7 +40,7 @@
             android:layout_height="match_parent"
             android:layout_margin="8dp"
             android:layout_weight="9"
             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"
             android:contentDescription="@string/profile_list_rearrange_handle_label"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toStartOf="parent"
@@ -70,7 +70,6 @@
 
     <TextView
         android:id="@+id/title"
 
     <TextView
         android:id="@+id/title"
-        style="@style/TextAppearance.AppCompat.Medium"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="8dp"
@@ -78,6 +77,7 @@
         android:gravity="center_vertical"
         android:paddingStart="@dimen/activity_horizontal_margin"
         android:text="Profile name"
         android:gravity="center_vertical"
         android:paddingStart="@dimen/activity_horizontal_margin"
         android:text="Profile name"
+        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/profile_list_edit_button"
         app:layout_constraintStart_toEndOf="@id/handle_and_tag"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toStartOf="@id/profile_list_edit_button"
         app:layout_constraintStart_toEndOf="@id/handle_and_tag"
diff --git a/app/src/main/res/layout/splash_activity_layout.xml b/app/src/main/res/layout/splash_activity_layout.xml
new file mode 100644 (file)
index 0000000..69947d2
--- /dev/null
@@ -0,0 +1,38 @@
+<?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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?colorPrimary"
+    >
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="@android:dimen/thumbnail_width"
+        android:layout_height="@android:dimen/thumbnail_height"
+        android:contentDescription="@string/splash_icon_description"
+        android:src="@drawable/app_icon_transparent_bg"
+        android:tint="@android:color/white"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/app_icon_transparent_bg"
+        />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
index 5835f1440fa6376db14e3a21ade7be5d9e1dcf25..e11755d03a0d626f3aa1077a35060f19fb83bbc6 100644 (file)
@@ -1,5 +1,5 @@
 <!--
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ 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: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_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerHorizontal="true"
-        android:layout_centerVertical="true" />
+        android:layout_centerVertical="true"
+        />
 </RelativeLayout>
 </RelativeLayout>
diff --git a/app/src/main/res/layout/template_details_account.xml b/app/src/main/res/layout/template_details_account.xml
new file mode 100644 (file)
index 0000000..d91bd57
--- /dev/null
@@ -0,0 +1,235 @@
+<?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
diff --git a/app/src/main/res/layout/template_details_fragment.xml b/app/src/main/res/layout/template_details_fragment.xml
new file mode 100644 (file)
index 0000000..2b7d575
--- /dev/null
@@ -0,0 +1,25 @@
+<?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
diff --git a/app/src/main/res/layout/template_details_header.xml b/app/src/main/res/layout/template_details_header.xml
new file mode 100644 (file)
index 0000000..28409d1
--- /dev/null
@@ -0,0 +1,428 @@
+<?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
diff --git a/app/src/main/res/layout/template_list_template_item.xml b/app/src/main/res/layout/template_list_template_item.xml
new file mode 100644 (file)
index 0000000..4cade77
--- /dev/null
@@ -0,0 +1,36 @@
+<?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
diff --git a/app/src/main/res/layout/templates_fallback_divider.xml b/app/src/main/res/layout/templates_fallback_divider.xml
new file mode 100644 (file)
index 0000000..bfeb0fa
--- /dev/null
@@ -0,0 +1,40 @@
+<?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
diff --git a/app/src/main/res/layout/transaction_delimiter.xml b/app/src/main/res/layout/transaction_delimiter.xml
new file mode 100644 (file)
index 0000000..0b283b3
--- /dev/null
@@ -0,0 +1,65 @@
+<?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
index 115a8a377a50a0a393b0fda6013f6fef5e77a1a2..60b1b1e001104c2053f4efb2960de1df7f4b150a 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?xml version="1.0" encoding="utf-8"?><!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -31,7 +31,6 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:theme="@style/ThemeOverlay.AppCompat.Light"
         android:visibility="gone">
 
         <TextView
         android:visibility="gone">
 
         <TextView
@@ -51,8 +50,8 @@
             android:id="@+id/clearAccountNameFilter"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:id="@+id/clearAccountNameFilter"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:background="@drawable/ic_clear_black_24dp"
-            android:backgroundTint="?colorAccent"
+            android:background="@drawable/ic_clear_accent_24dp"
+            android:backgroundTint="?colorSecondary"
             android:clickable="true"
             android:focusable="true" />
 
             android:clickable="true"
             android:focusable="true" />
 
index 5e398e7dcb80cf64a0c58f014a40dae1cd63e659..a91456ccc7c27c061cfc71a0a60b026810b29101 100644 (file)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
 <?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
   ~ This file is part of MoLe.
   ~ 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/>.
   -->
 
   ~ along with MoLe. If not, see <https://www.gnu.org/licenses/>.
   -->
 
-<androidx.appcompat.widget.ContentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout 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"
     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:layout_height="wrap_content"
+    >
 
 
-    <androidx.cardview.widget.CardView
+    <com.google.android.material.card.MaterialCardView
         android:id="@+id/transaction_card_view"
         android:id="@+id/transaction_card_view"
+        style="@style/Widget.MaterialComponents.CardView"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_margin="8dp"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_margin="8dp"
-        android:visibility="gone"
-        app:cardCornerRadius="16dp"
-        app:cardElevation="4dp"
+        android:visibility="visible"
+        app:cardCornerRadius="0dp"
+        app:cardElevation="2dp"
         app:cardUseCompatPadding="false"
         app:cardUseCompatPadding="false"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_goneMarginBottom="8dp">
+        >
 
         <androidx.constraintlayout.widget.ConstraintLayout
             android:id="@+id/transaction_row"
 
         <androidx.constraintlayout.widget.ConstraintLayout
             android:id="@+id/transaction_row"
             android:layout_height="wrap_content"
             android:gravity="center_vertical"
             android:minHeight="36dp"
             android:layout_height="wrap_content"
             android:gravity="center_vertical"
             android:minHeight="36dp"
-            android:orientation="horizontal"
-            android:padding="8dp">
+            android:padding="8dp"
+            >
 
             <LinearLayout
                 android:id="@+id/transaction_row_head"
 
             <LinearLayout
                 android:id="@+id/transaction_row_head"
-                android:layout_width="match_parent"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
-                android:orientation="horizontal"
+                android:orientation="vertical"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintEnd_toEndOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent">
+                app:layout_constraintTop_toTopOf="parent"
+                >
 
                 <TextView
                     android:id="@+id/transaction_row_description"
 
                 <TextView
                     android:id="@+id/transaction_row_description"
-                    style="@style/account_summary_account_name"
-                    android:layout_width="0dp"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_weight="5"
                     android:text="---."
                     android:text="---."
+                    android:textAppearance="@android:style/TextAppearance.Material.Medium"
                     android:textStyle="bold"
                     android:textStyle="bold"
-                    tools:ignore="HardcodedText" />
-
+                    tools:ignore="HardcodedText"
+                    />
+                <TextView
+                    android:id="@+id/transaction_comment"
+                    style="@style/transaction_list_comment"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="0dp"
+                    android:layout_marginTop="0dp"
+                    android:text="Comment text"
+                    tools:ignore="HardcodedText"
+                    />
             </LinearLayout>
 
             </LinearLayout>
 
-            <LinearLayout
-                android:id="@+id/transaction_row_header_border"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:background="@drawable/dashed_border_1dp"
-                android:alpha="0.3"
-                android:minHeight="2dp"
-                android:orientation="horizontal"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/transaction_row_head" />
-
             <LinearLayout
                 android:id="@+id/transaction_row_acc_amounts"
             <LinearLayout
                 android:id="@+id/transaction_row_acc_amounts"
-                android:layout_width="match_parent"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_weight="5"
+                android:layout_marginTop="8dp"
                 android:orientation="vertical"
                 android:orientation="vertical"
-                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintEnd_toStartOf="@id/transaction_running_total"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/transaction_row_header_border">
-
-                <LinearLayout
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:gravity="center_vertical"
-                    android:orientation="horizontal"
-                    android:paddingStart="8dp"
-                    android:paddingEnd="0dp">
-
-                    <TextView
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_weight="5"
-                        android:text="---"
-                        android:textAlignment="viewStart"
-                        tools:ignore="HardcodedText" />
-
-                    <TextView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:layout_marginEnd="0dp"
-                        android:minWidth="60dp"
-                        android:text="€ --,--"
-                        android:textAlignment="viewEnd"
-                        tools:ignore="HardcodedText" />
-                </LinearLayout>
+                app:layout_constraintTop_toBottomOf="@+id/transaction_row_head"
+                >
 
 
-                <LinearLayout
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:gravity="center_vertical"
-                    android:orientation="horizontal"
-                    android:paddingStart="8dp"
-                    android:paddingEnd="0dp">
+                <include layout="@layout/transaction_list_row_accounts_table_row" />
+                <include layout="@layout/transaction_list_row_accounts_table_row" />
 
 
-                    <TextView
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_weight="5"
-                        android:text="---"
-                        android:textAlignment="viewStart"
-                        tools:ignore="HardcodedText" />
-
-                    <TextView
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:layout_marginEnd="0dp"
-                        android:minWidth="60dp"
-                        android:text="---,--"
-                        android:textAlignment="viewEnd"
-                        tools:ignore="HardcodedText" />
-                </LinearLayout>
             </LinearLayout>
             </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>
 
         </androidx.constraintlayout.widget.ConstraintLayout>
-    </androidx.cardview.widget.CardView>
-
-    <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"
-        android:foregroundGravity="center_vertical"
-        android:orientation="horizontal">
-
-        <TextView
-            android:id="@+id/transaction_delimiter_month"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:background="?colorPrimary"
-            android:paddingStart="4dp"
-            android:paddingEnd="4dp"
-            android:text="---------"
-            android:textColor="@android:color/white"
-            android:textStyle="bold"
-            app:layout_constraintStart_toStartOf="parent"
-            tools:ignore="HardcodedText" />
-
-        <TextView
-            android:id="@+id/transaction_delimiter_date"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:background="?colorPrimary"
-            android:paddingStart="4dp"
-            android:paddingEnd="4dp"
-            android:text="--.--.----"
-            android:textColor="@android:color/white"
-            android:textStyle="bold"
-            app:layout_constraintEnd_toEndOf="parent"
-            tools:ignore="HardcodedText" />
-
-        <View
-            android:id="@+id/transaction_delimiter_line"
-            android:layout_width="0dp"
-            android:layout_height="16dp"
-            android:layout_marginStart="8dp"
-            android:layout_marginEnd="8dp"
-            android:background="@drawable/dashed_border_1dp"
-            android:alpha="0.3"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/transaction_delimiter_date"
-            app:layout_constraintStart_toEndOf="@id/transaction_delimiter_month"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <View
-            android:id="@+id/transaction_delimiter_thick"
-            android:layout_width="0dp"
-            android:layout_height="0dp"
-            android:background="?colorPrimary"
-            app:layout_constraintBottom_toBottomOf="parent"
-            app:layout_constraintEnd_toStartOf="@id/transaction_delimiter_date"
-            app:layout_constraintStart_toEndOf="@id/transaction_delimiter_month"
-            app:layout_constraintTop_toTopOf="parent" />
+    </com.google.android.material.card.MaterialCardView>
 
 
-    </androidx.constraintlayout.widget.ConstraintLayout>
 
 
-</androidx.appcompat.widget.ContentFrameLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/transaction_list_row_accounts_table_row.xml b/app/src/main/res/layout/transaction_list_row_accounts_table_row.xml
new file mode 100644 (file)
index 0000000..ac2853b
--- /dev/null
@@ -0,0 +1,76 @@
+<?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/>.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:orientation="horizontal"
+    >
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="5"
+        android:orientation="vertical"
+        >
+        <TextView
+            android:id="@+id/dummy_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:visibility="gone"
+            />
+
+        <TextView
+            android:id="@+id/transaction_list_acc_row_acc_name"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            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:id="@+id/transaction_list_acc_row_acc_comment"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="-4dp"
+            android:text="account comment"
+            android:textAlignment="viewStart"
+            style="@style/transaction_list_comment"
+            tools:ignore="HardcodedText,RtlSymmetry"
+            />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/transaction_list_acc_row_acc_amount"
+        android:layout_width="wrap_content"
+        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"
+        android:textAppearance="@android:style/TextAppearance.Material.Small"
+        tools:ignore="HardcodedText" />
+</LinearLayout>
diff --git a/app/src/main/res/menu/account_list.xml b/app/src/main/res/menu/account_list.xml
new file mode 100644 (file)
index 0000000..5a76161
--- /dev/null
@@ -0,0 +1,32 @@
+<?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
diff --git a/app/src/main/res/menu/account_summary.xml b/app/src/main/res/menu/account_summary.xml
deleted file mode 100644 (file)
index 28e9129..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<?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
diff --git a/app/src/main/res/menu/api_version.xml b/app/src/main/res/menu/api_version.xml
new file mode 100644 (file)
index 0000000..53da72f
--- /dev/null
@@ -0,0 +1,44 @@
+<?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/>.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/api_version_menu_auto"
+        android:title="@string/api_auto"
+        />
+    <item
+        android:id="@+id/api_version_menu_1_23"
+        android:title="@string/api_1_23"
+        />
+    <item
+        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"
+        />
+</menu>
\ No newline at end of file
index b318395bd00bbe4b317f63ac7d29726e1d7cff2b..bf88dac1482220cd448a5d0fee50d04a1ff27558 100644 (file)
@@ -19,6 +19,8 @@
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:title="@string/future_dates_none" android:id="@+id/menu_future_dates_none"/>
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:title="@string/future_dates_none" android:id="@+id/menu_future_dates_none"/>
+    <item android:title="@string/future_dates_7" android:id="@+id/menu_future_dates_7"/>
+    <item android:title="@string/future_dates_14" android:id="@+id/menu_future_dates_14"/>
     <item android:title="@string/future_dates_30" android:id="@+id/menu_future_dates_30"/>
     <item android:title="@string/future_dates_60" android:id="@+id/menu_future_dates_60"/>
     <item android:title="@string/future_dates_90" android:id="@+id/menu_future_dates_90"/>
     <item android:title="@string/future_dates_30" android:id="@+id/menu_future_dates_30"/>
     <item android:title="@string/future_dates_60" android:id="@+id/menu_future_dates_60"/>
     <item android:title="@string/future_dates_90" android:id="@+id/menu_future_dates_90"/>
index 060750803728d9bb0905680803133d37eb07819c..f4862d8c2e7a2f3a125af1eeb21bc37458cb4b64 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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"
   -->
 
 <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
 </menu>
\ No newline at end of file
index dc9b08dc992266d1e0928dfc43eab17d1abed3b0..b9a34687adc330e3ddf187cddf837273a350f5ae 100644 (file)
@@ -1,6 +1,5 @@
-<?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
   ~ This file is part of MoLe.
   ~ 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"
   -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-    <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
 </menu>
\ No newline at end of file
diff --git a/app/src/main/res/menu/profile_list.xml b/app/src/main/res/menu/profile_list.xml
deleted file mode 100644 (file)
index bb968ca..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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
diff --git a/app/src/main/res/menu/template_details_menu.xml b/app/src/main/res/menu/template_details_menu.xml
new file mode 100644 (file)
index 0000000..55a22a8
--- /dev/null
@@ -0,0 +1,28 @@
+<?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
diff --git a/app/src/main/res/menu/template_list_menu.xml b/app/src/main/res/menu/template_list_menu.xml
new file mode 100644 (file)
index 0000000..780d825
--- /dev/null
@@ -0,0 +1,28 @@
+<?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
index 323f4c520f4f9c296638fedd2b28bbc746a6608a..9cb272d45d34d3483724320eea5c31a35b439205 100644 (file)
@@ -1,6 +1,5 @@
-<?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
   ~ This file is part of MoLe.
   ~ 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/>.
   -->
 
   ~ 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">
+<menu 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"
+    >
 
     <item
 
     <item
-        tools:context="net.ktnx.mobileledger.ui.activity.MainActivity"
+        android:id="@+id/menu_go_to_date"
+        android:icon="@drawable/ic_event_black_24dp"
+        android:title="@string/go_to_date_menu_title"
+        app:showAsAction="ifRoom"
+        />
+    <item
         android:id="@+id/menu_transaction_list_filter"
         android:icon="@drawable/ic_filter_list_white_24dp"
         android:id="@+id/menu_transaction_list_filter"
         android:icon="@drawable/ic_filter_list_white_24dp"
-        android:title="Filter"
-        app:showAsAction="always" />
+        android:title="@string/filter_menu_title"
+        app:showAsAction="ifRoom"
+        tools:context="net.ktnx.mobileledger.ui.activity.MainActivity"
+        />
 </menu>
\ No newline at end of file
 </menu>
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644 (file)
index 898f3ed..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644 (file)
index dffca36..0000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644 (file)
index 64ba76f..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644 (file)
index dae5e08..0000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644 (file)
index e5ed465..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644 (file)
index 14ed0af..0000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644 (file)
index b0907ca..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644 (file)
index d8ae031..0000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644 (file)
index 2c18de9..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644 (file)
index beed3cd..0000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
index b68760c24e9f372924aa99007b419e2bf8c07f28..1178f88cac9ca63a6bf944f4b02ad0b87f6948d3 100644 (file)
@@ -1,6 +1,5 @@
-<?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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
 
 <navigation xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
 
 <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/new_transaction_navigation"
     android:id="@+id/new_transaction_navigation"
-    app:startDestination="@id/newTransactionFragment">
+    app:startDestination="@id/newTransactionFragment"
+    >
 
     <fragment
         android:id="@+id/newTransactionFragment"
 
     <fragment
         android:id="@+id/newTransactionFragment"
-        android:name="net.ktnx.mobileledger.ui.activity.NewTransactionFragment"
-        android:label="NewTransactionFragment" >
+        android:name="net.ktnx.mobileledger.ui.new_transaction.NewTransactionFragment"
+        android:label="NewTransactionFragment"
+        >
         <action
             android:id="@+id/action_newTransactionFragment_to_newTransactionSavingFragment"
             app:destination="@id/newTransactionSavingFragment"
             app:enterAnim="@anim/slide_in_up"
         <action
             android:id="@+id/action_newTransactionFragment_to_newTransactionSavingFragment"
             app:destination="@id/newTransactionSavingFragment"
             app:enterAnim="@anim/slide_in_up"
-            app:exitAnim="@anim/slide_out_up" />
+            app:exitAnim="@anim/slide_out_up"
+            app:launchSingleTop="true"
+            app:popUpTo="@id/new_transaction_navigation"
+            app:popUpToInclusive="true"
+            />
         <argument
             android:name="error"
         <argument
             android:name="error"
+            android:defaultValue="@null"
             app:argType="string"
             app:nullable="true"
             app:argType="string"
             app:nullable="true"
-            android:defaultValue="@null"/>
+            />
     </fragment>
     <fragment
         android:id="@+id/newTransactionSavingFragment"
         android:name="net.ktnx.mobileledger.ui.NewTransactionSavingFragment"
     </fragment>
     <fragment
         android:id="@+id/newTransactionSavingFragment"
         android:name="net.ktnx.mobileledger.ui.NewTransactionSavingFragment"
-        android:label="fragment_new_transaction_saving" >
+        android:label="fragment_new_transaction_saving"
+        >
         <action
             android:id="@+id/action_newTransactionSavingFragment_Success"
             app:destination="@id/newTransactionFragment"
             app:enterAnim="@anim/slide_in_up"
             app:exitAnim="@anim/slide_out_up"
         <action
             android:id="@+id/action_newTransactionSavingFragment_Success"
             app:destination="@id/newTransactionFragment"
             app:enterAnim="@anim/slide_in_up"
             app:exitAnim="@anim/slide_out_up"
+            app:launchSingleTop="true"
             app:popExitAnim="@anim/slide_out_down"
             app:popExitAnim="@anim/slide_out_down"
+            app:popUpTo="@id/new_transaction_navigation"
+            app:popUpToInclusive="true"
             />
         <action
             android:id="@+id/action_newTransactionSavingFragment_Failure"
             app:destination="@id/newTransactionFragment"
             app:enterAnim="@anim/slide_in_down"
             app:exitAnim="@anim/slide_out_down"
             />
         <action
             android:id="@+id/action_newTransactionSavingFragment_Failure"
             app:destination="@id/newTransactionFragment"
             app:enterAnim="@anim/slide_in_down"
             app:exitAnim="@anim/slide_out_down"
+            app:launchSingleTop="true"
             app:popExitAnim="@anim/slide_out_up"
             app:popExitAnim="@anim/slide_out_up"
+            app:popUpTo="@id/new_transaction_navigation"
+            app:popUpToInclusive="true"
             />
     </fragment>
 </navigation>
\ No newline at end of file
             />
     </fragment>
 </navigation>
\ No newline at end of file
diff --git a/app/src/main/res/navigation/template_list_navigation.xml b/app/src/main/res/navigation/template_list_navigation.xml
new file mode 100644 (file)
index 0000000..cbd32a1
--- /dev/null
@@ -0,0 +1,50 @@
+<?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
diff --git a/app/src/main/res/raw/create_db.sql b/app/src/main/res/raw/create_db.sql
deleted file mode 100644 (file)
index d077ded..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-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, expanded default 1, amounts_expanded boolean 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 '', keep boolean, value decimal not null );
-create unique index un_account_values on account_values(profile,account,currency);
-create table description_history(description varchar not null primary key, keep boolean, description_upper varchar);
-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);
-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));
diff --git a/app/src/main/res/raw/db_17.sql b/app/src/main/res/raw/db_17.sql
new file mode 100644 (file)
index 0000000..2da2ae3
--- /dev/null
@@ -0,0 +1,21 @@
+-- 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
diff --git a/app/src/main/res/raw/db_18.sql b/app/src/main/res/raw/db_18.sql
new file mode 100644 (file)
index 0000000..abe720b
--- /dev/null
@@ -0,0 +1,21 @@
+-- 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
diff --git a/app/src/main/res/raw/db_19.sql b/app/src/main/res/raw/db_19.sql
new file mode 100644 (file)
index 0000000..d278070
--- /dev/null
@@ -0,0 +1,21 @@
+-- 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
diff --git a/app/src/main/res/raw/db_20.sql b/app/src/main/res/raw/db_20.sql
new file mode 100644 (file)
index 0000000..7510b7d
--- /dev/null
@@ -0,0 +1,23 @@
+-- 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
diff --git a/app/src/main/res/raw/db_20_22.sql b/app/src/main/res/raw/db_20_22.sql
new file mode 100644 (file)
index 0000000..72301de
--- /dev/null
@@ -0,0 +1,19 @@
+-- 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
diff --git a/app/src/main/res/raw/db_22_30.sql b/app/src/main/res/raw/db_22_30.sql
new file mode 100644 (file)
index 0000000..e1760cc
--- /dev/null
@@ -0,0 +1,37 @@
+-- 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
diff --git a/app/src/main/res/raw/db_30_32.sql b/app/src/main/res/raw/db_30_32.sql
new file mode 100644 (file)
index 0000000..61ae734
--- /dev/null
@@ -0,0 +1,22 @@
+-- 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
diff --git a/app/src/main/res/raw/db_32_34.sql b/app/src/main/res/raw/db_32_34.sql
new file mode 100644 (file)
index 0000000..baa1ab1
--- /dev/null
@@ -0,0 +1,46 @@
+-- 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;
diff --git a/app/src/main/res/raw/db_34_40.sql b/app/src/main/res/raw/db_34_40.sql
new file mode 100644 (file)
index 0000000..c386749
--- /dev/null
@@ -0,0 +1,70 @@
+-- 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);
+
diff --git a/app/src/main/res/raw/db_41.sql b/app/src/main/res/raw/db_41.sql
new file mode 100644 (file)
index 0000000..1c9936d
--- /dev/null
@@ -0,0 +1,22 @@
+-- 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
diff --git a/app/src/main/res/raw/db_41_58.sql b/app/src/main/res/raw/db_41_58.sql
new file mode 100644 (file)
index 0000000..b9b40b2
--- /dev/null
@@ -0,0 +1,209 @@
+-- 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);
diff --git a/app/src/main/res/raw/db_59.sql b/app/src/main/res/raw/db_59.sql
new file mode 100644 (file)
index 0000000..effa07c
--- /dev/null
@@ -0,0 +1,184 @@
+-- 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
diff --git a/app/src/main/res/raw/db_60.sql b/app/src/main/res/raw/db_60.sql
new file mode 100644 (file)
index 0000000..bd94908
--- /dev/null
@@ -0,0 +1,27 @@
+-- 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;
diff --git a/app/src/main/res/raw/db_61.sql b/app/src/main/res/raw/db_61.sql
new file mode 100644 (file)
index 0000000..4b9dd69
--- /dev/null
@@ -0,0 +1,32 @@
+-- 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
diff --git a/app/src/main/res/raw/db_62.sql b/app/src/main/res/raw/db_62.sql
new file mode 100644 (file)
index 0000000..279400d
--- /dev/null
@@ -0,0 +1,29 @@
+-- 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
diff --git a/app/src/main/res/raw/db_63.sql b/app/src/main/res/raw/db_63.sql
new file mode 100644 (file)
index 0000000..4b11f7f
--- /dev/null
@@ -0,0 +1,63 @@
+-- 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
diff --git a/app/src/main/res/raw/db_64.sql b/app/src/main/res/raw/db_64.sql
new file mode 100644 (file)
index 0000000..e463e94
--- /dev/null
@@ -0,0 +1,62 @@
+-- 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
diff --git a/app/src/main/res/raw/sql_0.sql b/app/src/main/res/raw/sql_0.sql
deleted file mode 100644 (file)
index 3d05c51..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-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);
diff --git a/app/src/main/res/raw/sql_1.sql b/app/src/main/res/raw/sql_1.sql
deleted file mode 100644 (file)
index 2dfa141..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table accounts add keep boolean;
-alter table account_values add keep boolean;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_10.sql b/app/src/main/res/raw/sql_10.sql
deleted file mode 100644 (file)
index 48e7b79..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-delete from transaction_accounts;
-delete from transactions;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_11.sql b/app/src/main/res/raw/sql_11.sql
deleted file mode 100644 (file)
index 356aa16..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_12.sql b/app/src/main/res/raw/sql_12.sql
deleted file mode 100644 (file)
index f64bc5a..0000000
+++ /dev/null
@@ -1 +0,0 @@
-drop index un_profile_name;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_13.sql b/app/src/main/res/raw/sql_13.sql
deleted file mode 100644 (file)
index e8038e9..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_14.sql b/app/src/main/res/raw/sql_14.sql
deleted file mode 100644 (file)
index ddc634e..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_15.sql b/app/src/main/res/raw/sql_15.sql
deleted file mode 100644 (file)
index 56eb75e..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_16.sql b/app/src/main/res/raw/sql_16.sql
deleted file mode 100644 (file)
index 235f655..0000000
+++ /dev/null
@@ -1 +0,0 @@
-alter table profiles add order_no integer;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_17.sql b/app/src/main/res/raw/sql_17.sql
deleted file mode 100644 (file)
index f6f7eeb..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table profiles add permit_posting boolean default 0;
-update profiles set permit_posting = 1;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_18.sql b/app/src/main/res/raw/sql_18.sql
deleted file mode 100644 (file)
index bea40aa..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table profiles add theme integer default -1;
-update profiles set theme = -1;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_19.sql b/app/src/main/res/raw/sql_19.sql
deleted file mode 100644 (file)
index c2bdbaf..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table accounts add expanded default 1;
-update accounts set expanded = 1;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_2.sql b/app/src/main/res/raw/sql_2.sql
deleted file mode 100644 (file)
index bd27382..0000000
+++ /dev/null
@@ -1 +0,0 @@
-create table description_history(description varchar not null primary key, keep boolean);
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_20.sql b/app/src/main/res/raw/sql_20.sql
deleted file mode 100644 (file)
index 0485520..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_21.sql b/app/src/main/res/raw/sql_21.sql
deleted file mode 100644 (file)
index 19d37d8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-alter table accounts add amounts_expanded boolean default 0;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_22.sql b/app/src/main/res/raw/sql_22.sql
deleted file mode 100644 (file)
index 552397c..0000000
+++ /dev/null
@@ -1 +0,0 @@
-alter table profiles add preferred_accounts_filter varchar;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_23.sql b/app/src/main/res/raw/sql_23.sql
deleted file mode 100644 (file)
index ff3b8f7..0000000
+++ /dev/null
@@ -1 +0,0 @@
-alter table profiles add future_dates integer;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_24.sql b/app/src/main/res/raw/sql_24.sql
deleted file mode 100644 (file)
index 84ed1ae..0000000
+++ /dev/null
@@ -1 +0,0 @@
-alter table profiles add api_version integer;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_3.sql b/app/src/main/res/raw/sql_3.sql
deleted file mode 100644 (file)
index ceb373e..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_4.sql b/app/src/main/res/raw/sql_4.sql
deleted file mode 100644 (file)
index d1a14d7..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table accounts add hidden boolean default 0;
-update accounts set hidden = 0;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_5.sql b/app/src/main/res/raw/sql_5.sql
deleted file mode 100644 (file)
index 309f82e..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table accounts add level integer;
-alter table accounts add parent varchar;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_6.sql b/app/src/main/res/raw/sql_6.sql
deleted file mode 100644 (file)
index c8e8634..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-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;
diff --git a/app/src/main/res/raw/sql_7.sql b/app/src/main/res/raw/sql_7.sql
deleted file mode 100644 (file)
index 5217e5d..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-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
diff --git a/app/src/main/res/raw/sql_8.sql b/app/src/main/res/raw/sql_8.sql
deleted file mode 100644 (file)
index 0f06539..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alter table transactions add data_hash varchar;
-delete from transactions;
\ No newline at end of file
diff --git a/app/src/main/res/raw/sql_9.sql b/app/src/main/res/raw/sql_9.sql
deleted file mode 100644 (file)
index 936cec6..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-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
index 9d5035f473229c31c618576db9b7dd33eeabe946..88598bf65a4e2e107a3fae340c007338443fec47 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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-array name="acc_ctx_menu">
 
 <resources>
     <string-array name="acc_ctx_menu">
-        <item>Показване на трансакциите</item>
+        <item>Показване на транзакциите</item>
         <!--<item>Expand all</item>-->
         <!--<item>Collapse all</item>-->
     </string-array>
         <!--<item>Expand all</item>-->
         <!--<item>Collapse all</item>-->
     </string-array>
-</resources>
\ No newline at end of file
+    <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>
index b1f6c4dc5cbf55b208275cfc52f40ff9f1b894c6..ccb47516c7ce94db047f23881c99479810dc84c8 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 <?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
   ~ This file is part of MoLe.
   ~ 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>
   -->
 
 <resources>
-    <string name="title_activity_settings">Настройки</string>
-    <string name="pref_header_backend">Сървър</string>
     <string name="pref_title_use_http_auth">Удостоверяване</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="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анÑ\81акÑ\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="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="new_transaction_description_hint">Описание</string>
-    <string name="progress_N_accounts_loaded">Заредени са %d сметки</string>
     <string name="account_summary_title">Сметки</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="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="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>
     <string name="new_profile_title">Нов профил</string>
     <string name="delete_profile">Изтриване на профила</string>
     <string name="delete">Изтриване</string>
@@ -75,7 +48,6 @@
         <item>Ноември</item>
         <item>Декември</item>
     </string-array>
         <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="url_label">Адрес</string>
     <string name="profile_name_label">Име на профила</string>
     <string name="create_profile_label">Създаване на профил</string>
@@ -85,7 +57,7 @@
     <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="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="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="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_subitlte_read_only">(Само за преглед)</string>
-    <string name="menu_acc_summary_confirm_selection_title">Потвърждаване на избора</string>
-    <string name="menu_acc_summary_hide_selected_title">Скриване на маркираните сметки</string>
+    <string name="profile_subtitle_read_only">(Само за преглед)</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_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="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="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="err_invalid_url">Грешен или непълен адрес</string>
     <string name="btn_color_picker_button">Бутон за избор на цвят</string>
     <string name="insecure_scheme_with_auth">ВНИМАНИЕ: Използване на удостоверяване с несигурна схема на достъп</string>
     <string name="simulate_save_condensed_label">Симул. съхр.</string>
     <string name="simulation_label">СИМУЛАЦИЯ</string>
     <string name="future_dates_180">До шест месеца</string>
     <string name="simulate_save_condensed_label">Симул. съхр.</string>
     <string name="simulation_label">СИМУЛАЦИЯ</string>
     <string name="future_dates_180">До шест месеца</string>
+    <string name="future_dates_7">До една седмица</string>
+    <string name="future_dates_14">До две седмици</string>
     <string name="future_dates_30">До един месец</string>
     <string name="future_dates_365">До една година</string>
     <string name="future_dates_60">До два месеца</string>
     <string name="future_dates_30">До един месец</string>
     <string name="future_dates_365">До една година</string>
     <string name="future_dates_60">До два месеца</string>
     <string name="future_dates_all">Без ограничения</string>
     <string name="future_dates_none">Без въвеждане на бъдещи дати</string>
     <string name="profile_future_dates_label">Въвеждане на дати в бъдещето</string>
     <string name="future_dates_all">Без ограничения</string>
     <string name="future_dates_none">Без въвеждане на бъдещи дати</string>
     <string name="profile_future_dates_label">Въвеждане на дати в бъдещето</string>
-    <string name="api_auto">Автоматично откриване</string>
-    <string name="api_html">Симулиране на заявка от браузър</string>
-    <string name="api_post_1_14">версия 1.15 и по-нови</string>
-    <string name="api_pre_1_15">версии преди 1.15</string>
-
+    <string name="api_auto">Автоматична</string>
+    <string name="api_html">Версия преди 1.14</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="btn_no_currency">без</string>
+    <string name="choose_currency_label">Валута</string>
+    <string name="currency_has_gap">Отстояние от сумата</string>
+    <string name="currency_position_left">Вляво</string>
+    <string name="currency_position_right">Вдясно</string>
+    <string name="new_currency_name_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="show_comments_switch">Коментари</string>
+    <string name="show_comment_input_by_default">Показване по подразбиране на полетата за бележки</string>
+    <string name="icon">икона</string>
+    <string name="splash_icon_description">Икона на приложението</string>
+    <string name="sub_accounts_expand_collapse_trigger_description">Превключвател за разширяване/свиване на под-сметките</string>
+    <string name="go_to_date_menu_title">Към дата</string>
+    <string name="filter_menu_title">Филтър</string>
+    <string name="navigation_drawer_open">Отваряне на страничния панел</string>
+    <string name="navigation_drawer_close">Затваряне на страничния панел</string>
+    <string name="nav_header_desc">Заглавна част на страничния панел</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>
 </resources>
index 126fd4fcbf2129132723054750493e5bcbbc2326..586a952975b2dab8f2b1922463ba2547932ff4f2 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -16,6 +16,5 @@
   -->
 
 <resources>
   -->
 
 <resources>
-    <dimen name="nav_header_vertical_spacing">8dp</dimen>
     <dimen name="activity_vertical_margin">16dp</dimen>
 </resources>
\ No newline at end of file
     <dimen name="activity_vertical_margin">16dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
new file mode 100644 (file)
index 0000000..1489a08
--- /dev/null
@@ -0,0 +1,737 @@
+<!--
+  ~ 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/>.
+  -->
+
+<resources>
+    <!-- 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="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+        <item name="textColor">#ffffff</item>
+        <item name="commentColor">#909090</item>
+        <item name="colorOnSecondary">#dddddd</item>
+        <item name="colorOnSurface">@android:color/white</item>
+        <item name="colorOnPrimarySurface">@android:color/white</item>
+        <item name="colorOnBackground">@android:color/white</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="shadowStartColor">#C0000000</item>
+        <item name="shadowEndColor">#00000000</item>
+    </style>
+
+    <!-- theme list start -->
+
+    <style name="AppTheme.default" parent="AppTheme">
+        <item name="colorPrimary">#935ff2</item>
+        <item name="colorPrimaryTransparent">#00935ff2</item>
+        <item name="colorSecondary">#935ff2</item>
+        <item name="colorPrimaryDark">#6f35d8</item>
+        <item name="table_row_dark_bg">#1d0647</item>
+        <item name="table_row_light_bg">#13042f</item>
+    </style>
+
+    <style name="AppTheme.000" parent="AppTheme">
+        <item name="colorPrimary">#ee3232</item>
+        <item name="colorPrimaryTransparent">#00ee3232</item>
+        <item name="colorSecondary">#ee3232</item>
+        <item name="colorPrimaryDark">#c22525</item>
+        <item name="table_row_dark_bg">#470606</item>
+        <item name="table_row_light_bg">#2f0404</item>
+    </style>
+
+    <style name="AppTheme.005" parent="AppTheme">
+        <item name="colorPrimary">#ed3625</item>
+        <item name="colorPrimaryTransparent">#00ed3625</item>
+        <item name="colorSecondary">#ed3625</item>
+        <item name="colorPrimaryDark">#b83023</item>
+        <item name="table_row_dark_bg">#470b06</item>
+        <item name="table_row_light_bg">#2f0704</item>
+    </style>
+
+    <style name="AppTheme.010" parent="AppTheme">
+        <item name="colorPrimary">#ec3915</item>
+        <item name="colorPrimaryTransparent">#00ec3915</item>
+        <item name="colorSecondary">#ec3915</item>
+        <item name="colorPrimaryDark">#ad3821</item>
+        <item name="table_row_dark_bg">#471106</item>
+        <item name="table_row_light_bg">#2f0b04</item>
+    </style>
+
+    <style name="AppTheme.015" parent="AppTheme">
+        <item name="colorPrimary">#e24612</item>
+        <item name="colorPrimaryTransparent">#00e24612</item>
+        <item name="colorSecondary">#e24612</item>
+        <item name="colorPrimaryDark">#a4411f</item>
+        <item name="table_row_dark_bg">#471606</item>
+        <item name="table_row_light_bg">#2f0f04</item>
+    </style>
+
+    <style name="AppTheme.020" parent="AppTheme">
+        <item name="colorPrimary">#d75311</item>
+        <item name="colorPrimaryTransparent">#00d75311</item>
+        <item name="colorSecondary">#d75311</item>
+        <item name="colorPrimaryDark">#9c481e</item>
+        <item name="table_row_dark_bg">#471b06</item>
+        <item name="table_row_light_bg">#2f1204</item>
+    </style>
+
+    <style name="AppTheme.025" parent="AppTheme">
+        <item name="colorPrimary">#cb5e10</item>
+        <item name="colorPrimaryTransparent">#00cb5e10</item>
+        <item name="colorSecondary">#cb5e10</item>
+        <item name="colorPrimaryDark">#934e1c</item>
+        <item name="table_row_dark_bg">#472106</item>
+        <item name="table_row_light_bg">#2f1604</item>
+    </style>
+
+    <style name="AppTheme.030" parent="AppTheme">
+        <item name="colorPrimary">#bf670f</item>
+        <item name="colorPrimaryTransparent">#00bf670f</item>
+        <item name="colorSecondary">#bf670f</item>
+        <item name="colorPrimaryDark">#8a521a</item>
+        <item name="table_row_dark_bg">#472606</item>
+        <item name="table_row_light_bg">#2f1a04</item>
+    </style>
+
+    <style name="AppTheme.035" parent="AppTheme">
+        <item name="colorPrimary">#b26e0e</item>
+        <item name="colorPrimaryTransparent">#00b26e0e</item>
+        <item name="colorSecondary">#b26e0e</item>
+        <item name="colorPrimaryDark">#825619</item>
+        <item name="table_row_dark_bg">#472c06</item>
+        <item name="table_row_light_bg">#2f1d04</item>
+    </style>
+
+    <style name="AppTheme.040" parent="AppTheme">
+        <item name="colorPrimary">#a7740e</item>
+        <item name="colorPrimaryTransparent">#00a7740e</item>
+        <item name="colorSecondary">#a7740e</item>
+        <item name="colorPrimaryDark">#795917</item>
+        <item name="table_row_dark_bg">#473106</item>
+        <item name="table_row_light_bg">#2f2104</item>
+    </style>
+
+    <style name="AppTheme.045" parent="AppTheme">
+        <item name="colorPrimary">#9d790d</item>
+        <item name="colorPrimaryTransparent">#009d790d</item>
+        <item name="colorSecondary">#9d790d</item>
+        <item name="colorPrimaryDark">#725b16</item>
+        <item name="table_row_dark_bg">#473706</item>
+        <item name="table_row_light_bg">#2f2404</item>
+    </style>
+
+    <style name="AppTheme.050" parent="AppTheme">
+        <item name="colorPrimary">#937d0c</item>
+        <item name="colorPrimaryTransparent">#00937d0c</item>
+        <item name="colorSecondary">#937d0c</item>
+        <item name="colorPrimaryDark">#6b5c14</item>
+        <item name="table_row_dark_bg">#473c06</item>
+        <item name="table_row_light_bg">#2f2804</item>
+    </style>
+
+    <style name="AppTheme.055" parent="AppTheme">
+        <item name="colorPrimary">#8b800b</item>
+        <item name="colorPrimaryTransparent">#008b800b</item>
+        <item name="colorSecondary">#8b800b</item>
+        <item name="colorPrimaryDark">#655e13</item>
+        <item name="table_row_dark_bg">#474106</item>
+        <item name="table_row_light_bg">#2f2c04</item>
+    </style>
+
+    <style name="AppTheme.060" parent="AppTheme">
+        <item name="colorPrimary">#82820b</item>
+        <item name="colorPrimaryTransparent">#0082820b</item>
+        <item name="colorSecondary">#82820b</item>
+        <item name="colorPrimaryDark">#5f5f12</item>
+        <item name="table_row_dark_bg">#474706</item>
+        <item name="table_row_light_bg">#2f2f04</item>
+    </style>
+
+    <style name="AppTheme.065" parent="AppTheme">
+        <item name="colorPrimary">#7a840b</item>
+        <item name="colorPrimaryTransparent">#007a840b</item>
+        <item name="colorSecondary">#7a840b</item>
+        <item name="colorPrimaryDark">#596012</item>
+        <item name="table_row_dark_bg">#414706</item>
+        <item name="table_row_light_bg">#2c2f04</item>
+    </style>
+
+    <style name="AppTheme.070" parent="AppTheme">
+        <item name="colorPrimary">#72870b</item>
+        <item name="colorPrimaryTransparent">#0072870b</item>
+        <item name="colorSecondary">#72870b</item>
+        <item name="colorPrimaryDark">#556213</item>
+        <item name="table_row_dark_bg">#3c4706</item>
+        <item name="table_row_light_bg">#282f04</item>
+    </style>
+
+    <style name="AppTheme.075" parent="AppTheme">
+        <item name="colorPrimary">#69890b</item>
+        <item name="colorPrimaryTransparent">#0069890b</item>
+        <item name="colorSecondary">#69890b</item>
+        <item name="colorPrimaryDark">#4f6313</item>
+        <item name="table_row_dark_bg">#374706</item>
+        <item name="table_row_light_bg">#242f04</item>
+    </style>
+
+    <style name="AppTheme.080" parent="AppTheme">
+        <item name="colorPrimary">#608b0b</item>
+        <item name="colorPrimaryTransparent">#00608b0b</item>
+        <item name="colorSecondary">#608b0b</item>
+        <item name="colorPrimaryDark">#4a6513</item>
+        <item name="table_row_dark_bg">#314706</item>
+        <item name="table_row_light_bg">#212f04</item>
+    </style>
+
+    <style name="AppTheme.085" parent="AppTheme">
+        <item name="colorPrimary">#568c0b</item>
+        <item name="colorPrimaryTransparent">#00568c0b</item>
+        <item name="colorSecondary">#568c0b</item>
+        <item name="colorPrimaryDark">#436513</item>
+        <item name="table_row_dark_bg">#2c4706</item>
+        <item name="table_row_light_bg">#1d2f04</item>
+    </style>
+
+    <style name="AppTheme.090" parent="AppTheme">
+        <item name="colorPrimary">#4d8e0b</item>
+        <item name="colorPrimaryTransparent">#004d8e0b</item>
+        <item name="colorSecondary">#4d8e0b</item>
+        <item name="colorPrimaryDark">#3d6714</item>
+        <item name="table_row_dark_bg">#264706</item>
+        <item name="table_row_light_bg">#1a2f04</item>
+    </style>
+
+    <style name="AppTheme.095" parent="AppTheme">
+        <item name="colorPrimary">#428e0c</item>
+        <item name="colorPrimaryTransparent">#00428e0c</item>
+        <item name="colorSecondary">#428e0c</item>
+        <item name="colorPrimaryDark">#376714</item>
+        <item name="table_row_dark_bg">#214706</item>
+        <item name="table_row_light_bg">#162f04</item>
+    </style>
+
+    <style name="AppTheme.100" parent="AppTheme">
+        <item name="colorPrimary">#38900c</item>
+        <item name="colorPrimaryTransparent">#0038900c</item>
+        <item name="colorSecondary">#38900c</item>
+        <item name="colorPrimaryDark">#306914</item>
+        <item name="table_row_dark_bg">#1b4706</item>
+        <item name="table_row_light_bg">#122f04</item>
+    </style>
+
+    <style name="AppTheme.105" parent="AppTheme">
+        <item name="colorPrimary">#2d910c</item>
+        <item name="colorPrimaryTransparent">#002d910c</item>
+        <item name="colorSecondary">#2d910c</item>
+        <item name="colorPrimaryDark">#296a14</item>
+        <item name="table_row_dark_bg">#164706</item>
+        <item name="table_row_light_bg">#0f2f04</item>
+    </style>
+
+    <style name="AppTheme.110" parent="AppTheme">
+        <item name="colorPrimary">#22910c</item>
+        <item name="colorPrimaryTransparent">#0022910c</item>
+        <item name="colorSecondary">#22910c</item>
+        <item name="colorPrimaryDark">#226a14</item>
+        <item name="table_row_dark_bg">#114706</item>
+        <item name="table_row_light_bg">#0b2f04</item>
+    </style>
+
+    <style name="AppTheme.115" parent="AppTheme">
+        <item name="colorPrimary">#17920c</item>
+        <item name="colorPrimaryTransparent">#0017920c</item>
+        <item name="colorSecondary">#17920c</item>
+        <item name="colorPrimaryDark">#1b6a14</item>
+        <item name="table_row_dark_bg">#0b4706</item>
+        <item name="table_row_light_bg">#072f04</item>
+    </style>
+
+    <style name="AppTheme.120" parent="AppTheme">
+        <item name="colorPrimary">#0c920c</item>
+        <item name="colorPrimaryTransparent">#000c920c</item>
+        <item name="colorSecondary">#0c920c</item>
+        <item name="colorPrimaryDark">#146a14</item>
+        <item name="table_row_dark_bg">#064706</item>
+        <item name="table_row_light_bg">#042f04</item>
+    </style>
+
+    <style name="AppTheme.125" parent="AppTheme">
+        <item name="colorPrimary">#0c9217</item>
+        <item name="colorPrimaryTransparent">#000c9217</item>
+        <item name="colorSecondary">#0c9217</item>
+        <item name="colorPrimaryDark">#146a1b</item>
+        <item name="table_row_dark_bg">#06470b</item>
+        <item name="table_row_light_bg">#042f07</item>
+    </style>
+
+    <style name="AppTheme.130" parent="AppTheme">
+        <item name="colorPrimary">#0c9222</item>
+        <item name="colorPrimaryTransparent">#000c9222</item>
+        <item name="colorSecondary">#0c9222</item>
+        <item name="colorPrimaryDark">#146a23</item>
+        <item name="table_row_dark_bg">#064711</item>
+        <item name="table_row_light_bg">#042f0b</item>
+    </style>
+
+    <style name="AppTheme.135" parent="AppTheme">
+        <item name="colorPrimary">#0c922d</item>
+        <item name="colorPrimaryTransparent">#000c922d</item>
+        <item name="colorSecondary">#0c922d</item>
+        <item name="colorPrimaryDark">#146a2a</item>
+        <item name="table_row_dark_bg">#064716</item>
+        <item name="table_row_light_bg">#042f0f</item>
+    </style>
+
+    <style name="AppTheme.140" parent="AppTheme">
+        <item name="colorPrimary">#0c9138</item>
+        <item name="colorPrimaryTransparent">#000c9138</item>
+        <item name="colorSecondary">#0c9138</item>
+        <item name="colorPrimaryDark">#146a31</item>
+        <item name="table_row_dark_bg">#06471b</item>
+        <item name="table_row_light_bg">#042f12</item>
+    </style>
+
+    <style name="AppTheme.145" parent="AppTheme">
+        <item name="colorPrimary">#0c9143</item>
+        <item name="colorPrimaryTransparent">#000c9143</item>
+        <item name="colorSecondary">#0c9143</item>
+        <item name="colorPrimaryDark">#146a38</item>
+        <item name="table_row_dark_bg">#064721</item>
+        <item name="table_row_light_bg">#042f16</item>
+    </style>
+
+    <style name="AppTheme.150" parent="AppTheme">
+        <item name="colorPrimary">#0c904e</item>
+        <item name="colorPrimaryTransparent">#000c904e</item>
+        <item name="colorSecondary">#0c904e</item>
+        <item name="colorPrimaryDark">#14693e</item>
+        <item name="table_row_dark_bg">#064726</item>
+        <item name="table_row_light_bg">#042f1a</item>
+    </style>
+
+    <style name="AppTheme.155" parent="AppTheme">
+        <item name="colorPrimary">#0c9059</item>
+        <item name="colorPrimaryTransparent">#000c9059</item>
+        <item name="colorSecondary">#0c9059</item>
+        <item name="colorPrimaryDark">#146945</item>
+        <item name="table_row_dark_bg">#06472c</item>
+        <item name="table_row_light_bg">#042f1d</item>
+    </style>
+
+    <style name="AppTheme.160" parent="AppTheme">
+        <item name="colorPrimary">#0c8f63</item>
+        <item name="colorPrimaryTransparent">#000c8f63</item>
+        <item name="colorSecondary">#0c8f63</item>
+        <item name="colorPrimaryDark">#14684c</item>
+        <item name="table_row_dark_bg">#064731</item>
+        <item name="table_row_light_bg">#042f21</item>
+    </style>
+
+    <style name="AppTheme.165" parent="AppTheme">
+        <item name="colorPrimary">#0c8e6e</item>
+        <item name="colorPrimaryTransparent">#000c8e6e</item>
+        <item name="colorSecondary">#0c8e6e</item>
+        <item name="colorPrimaryDark">#146753</item>
+        <item name="table_row_dark_bg">#064737</item>
+        <item name="table_row_light_bg">#042f24</item>
+    </style>
+
+    <style name="AppTheme.170" parent="AppTheme">
+        <item name="colorPrimary">#0c8e79</item>
+        <item name="colorPrimaryTransparent">#000c8e79</item>
+        <item name="colorSecondary">#0c8e79</item>
+        <item name="colorPrimaryDark">#14675a</item>
+        <item name="table_row_dark_bg">#06473c</item>
+        <item name="table_row_light_bg">#042f28</item>
+    </style>
+
+    <style name="AppTheme.175" parent="AppTheme">
+        <item name="colorPrimary">#0b8e83</item>
+        <item name="colorPrimaryTransparent">#000b8e83</item>
+        <item name="colorSecondary">#0b8e83</item>
+        <item name="colorPrimaryDark">#146760</item>
+        <item name="table_row_dark_bg">#064741</item>
+        <item name="table_row_light_bg">#042f2c</item>
+    </style>
+
+    <style name="AppTheme.180" parent="AppTheme">
+        <item name="colorPrimary">#0b8d8d</item>
+        <item name="colorPrimaryTransparent">#000b8d8d</item>
+        <item name="colorSecondary">#0b8d8d</item>
+        <item name="colorPrimaryDark">#136666</item>
+        <item name="table_row_dark_bg">#064747</item>
+        <item name="table_row_light_bg">#042f2f</item>
+    </style>
+
+    <style name="AppTheme.185" parent="AppTheme">
+        <item name="colorPrimary">#0c8b97</item>
+        <item name="colorPrimaryTransparent">#000c8b97</item>
+        <item name="colorSecondary">#0c8b97</item>
+        <item name="colorPrimaryDark">#15666e</item>
+        <item name="table_row_dark_bg">#064147</item>
+        <item name="table_row_light_bg">#042c2f</item>
+    </style>
+
+    <style name="AppTheme.190" parent="AppTheme">
+        <item name="colorPrimary">#0d89a2</item>
+        <item name="colorPrimaryTransparent">#000d89a2</item>
+        <item name="colorSecondary">#0d89a2</item>
+        <item name="colorPrimaryDark">#166676</item>
+        <item name="table_row_dark_bg">#063c47</item>
+        <item name="table_row_light_bg">#04282f</item>
+    </style>
+
+    <style name="AppTheme.195" parent="AppTheme">
+        <item name="colorPrimary">#0e88b0</item>
+        <item name="colorPrimaryTransparent">#000e88b0</item>
+        <item name="colorSecondary">#0e88b0</item>
+        <item name="colorPrimaryDark">#186680</item>
+        <item name="table_row_dark_bg">#063747</item>
+        <item name="table_row_light_bg">#04242f</item>
+    </style>
+
+    <style name="AppTheme.200" parent="AppTheme">
+        <item name="colorPrimary">#1086c0</item>
+        <item name="colorPrimaryTransparent">#001086c0</item>
+        <item name="colorSecondary">#1086c0</item>
+        <item name="colorPrimaryDark">#1b668c</item>
+        <item name="table_row_dark_bg">#063147</item>
+        <item name="table_row_light_bg">#04212f</item>
+    </style>
+
+    <style name="AppTheme.205" parent="AppTheme">
+        <item name="colorPrimary">#1182d2</item>
+        <item name="colorPrimaryTransparent">#001182d2</item>
+        <item name="colorSecondary">#1182d2</item>
+        <item name="colorPrimaryDark">#1d6599</item>
+        <item name="table_row_dark_bg">#062c47</item>
+        <item name="table_row_light_bg">#041d2f</item>
+    </style>
+
+    <style name="AppTheme.210" parent="AppTheme">
+        <item name="colorPrimary">#137de8</item>
+        <item name="colorPrimaryTransparent">#00137de8</item>
+        <item name="colorSecondary">#137de8</item>
+        <item name="colorPrimaryDark">#2064a9</item>
+        <item name="table_row_dark_bg">#062647</item>
+        <item name="table_row_light_bg">#041a2f</item>
+    </style>
+
+    <style name="AppTheme.215" parent="AppTheme">
+        <item name="colorPrimary">#297bee</item>
+        <item name="colorPrimaryTransparent">#00297bee</item>
+        <item name="colorSecondary">#297bee</item>
+        <item name="colorPrimaryDark">#2463bb</item>
+        <item name="table_row_dark_bg">#062147</item>
+        <item name="table_row_light_bg">#04162f</item>
+    </style>
+
+    <style name="AppTheme.220" parent="AppTheme">
+        <item name="colorPrimary">#3e79ef</item>
+        <item name="colorPrimaryTransparent">#003e79ef</item>
+        <item name="colorSecondary">#3e79ef</item>
+        <item name="colorPrimaryDark">#275dca</item>
+        <item name="table_row_dark_bg">#061b47</item>
+        <item name="table_row_light_bg">#04122f</item>
+    </style>
+
+    <style name="AppTheme.225" parent="AppTheme">
+        <item name="colorPrimary">#4d76f1</item>
+        <item name="colorPrimaryTransparent">#004d76f1</item>
+        <item name="colorSecondary">#4d76f1</item>
+        <item name="colorPrimaryDark">#2954d5</item>
+        <item name="table_row_dark_bg">#061647</item>
+        <item name="table_row_light_bg">#040f2f</item>
+    </style>
+
+    <style name="AppTheme.230" parent="AppTheme">
+        <item name="colorPrimary">#5a73f2</item>
+        <item name="colorPrimaryTransparent">#005a73f2</item>
+        <item name="colorSecondary">#5a73f2</item>
+        <item name="colorPrimaryDark">#314dd8</item>
+        <item name="table_row_dark_bg">#061147</item>
+        <item name="table_row_light_bg">#040b2f</item>
+    </style>
+
+    <style name="AppTheme.235" parent="AppTheme">
+        <item name="colorPrimary">#6470f2</item>
+        <item name="colorPrimaryTransparent">#006470f2</item>
+        <item name="colorSecondary">#6470f2</item>
+        <item name="colorPrimaryDark">#3946d9</item>
+        <item name="table_row_dark_bg">#060b47</item>
+        <item name="table_row_light_bg">#04072f</item>
+    </style>
+
+    <style name="AppTheme.240" parent="AppTheme">
+        <item name="colorPrimary">#6e6ef3</item>
+        <item name="colorPrimaryTransparent">#006e6ef3</item>
+        <item name="colorSecondary">#6e6ef3</item>
+        <item name="colorPrimaryDark">#4040db</item>
+        <item name="table_row_dark_bg">#060647</item>
+        <item name="table_row_light_bg">#04042f</item>
+    </style>
+
+    <style name="AppTheme.245" parent="AppTheme">
+        <item name="colorPrimary">#766bf3</item>
+        <item name="colorPrimaryTransparent">#00766bf3</item>
+        <item name="colorSecondary">#766bf3</item>
+        <item name="colorPrimaryDark">#4b3eda</item>
+        <item name="table_row_dark_bg">#0b0647</item>
+        <item name="table_row_light_bg">#07042f</item>
+    </style>
+
+    <style name="AppTheme.250" parent="AppTheme">
+        <item name="colorPrimary">#7f68f3</item>
+        <item name="colorPrimaryTransparent">#007f68f3</item>
+        <item name="colorSecondary">#7f68f3</item>
+        <item name="colorPrimaryDark">#563cda</item>
+        <item name="table_row_dark_bg">#110647</item>
+        <item name="table_row_light_bg">#0b042f</item>
+    </style>
+
+    <style name="AppTheme.255" parent="AppTheme">
+        <item name="colorPrimary">#8864f2</item>
+        <item name="colorPrimaryTransparent">#008864f2</item>
+        <item name="colorSecondary">#8864f2</item>
+        <item name="colorPrimaryDark">#6139d9</item>
+        <item name="table_row_dark_bg">#160647</item>
+        <item name="table_row_light_bg">#0f042f</item>
+    </style>
+
+    <style name="AppTheme.260" parent="AppTheme">
+        <item name="colorPrimary">#9161f2</item>
+        <item name="colorPrimaryTransparent">#009161f2</item>
+        <item name="colorSecondary">#9161f2</item>
+        <item name="colorPrimaryDark">#6c36d9</item>
+        <item name="table_row_dark_bg">#1b0647</item>
+        <item name="table_row_light_bg">#12042f</item>
+    </style>
+
+    <style name="AppTheme.265" parent="AppTheme">
+        <item name="colorPrimary">#9a5bf2</item>
+        <item name="colorPrimaryTransparent">#009a5bf2</item>
+        <item name="colorSecondary">#9a5bf2</item>
+        <item name="colorPrimaryDark">#7732d8</item>
+        <item name="table_row_dark_bg">#210647</item>
+        <item name="table_row_light_bg">#16042f</item>
+    </style>
+
+    <style name="AppTheme.270" parent="AppTheme">
+        <item name="colorPrimary">#a355f1</item>
+        <item name="colorPrimaryTransparent">#00a355f1</item>
+        <item name="colorSecondary">#a355f1</item>
+        <item name="colorPrimaryDark">#832ed7</item>
+        <item name="table_row_dark_bg">#260647</item>
+        <item name="table_row_light_bg">#1a042f</item>
+    </style>
+
+    <style name="AppTheme.275" parent="AppTheme">
+        <item name="colorPrimary">#ad4ff1</item>
+        <item name="colorPrimaryTransparent">#00ad4ff1</item>
+        <item name="colorSecondary">#ad4ff1</item>
+        <item name="colorPrimaryDark">#8e29d6</item>
+        <item name="table_row_dark_bg">#2c0647</item>
+        <item name="table_row_light_bg">#1d042f</item>
+    </style>
+
+    <style name="AppTheme.280" parent="AppTheme">
+        <item name="colorPrimary">#b746f0</item>
+        <item name="colorPrimaryTransparent">#00b746f0</item>
+        <item name="colorSecondary">#b746f0</item>
+        <item name="colorPrimaryDark">#9828d0</item>
+        <item name="table_row_dark_bg">#310647</item>
+        <item name="table_row_light_bg">#21042f</item>
+    </style>
+
+    <style name="AppTheme.285" parent="AppTheme">
+        <item name="colorPrimary">#c23bef</item>
+        <item name="colorPrimaryTransparent">#00c23bef</item>
+        <item name="colorSecondary">#c23bef</item>
+        <item name="colorPrimaryDark">#a026c8</item>
+        <item name="table_row_dark_bg">#370647</item>
+        <item name="table_row_light_bg">#24042f</item>
+    </style>
+
+    <style name="AppTheme.290" parent="AppTheme">
+        <item name="colorPrimary">#cd2aee</item>
+        <item name="colorPrimaryTransparent">#00cd2aee</item>
+        <item name="colorSecondary">#cd2aee</item>
+        <item name="colorPrimaryDark">#a224bc</item>
+        <item name="table_row_dark_bg">#3c0647</item>
+        <item name="table_row_light_bg">#28042f</item>
+    </style>
+
+    <style name="AppTheme.295" parent="AppTheme">
+        <item name="colorPrimary">#d713e9</item>
+        <item name="colorPrimaryTransparent">#00d713e9</item>
+        <item name="colorSecondary">#d713e9</item>
+        <item name="colorPrimaryDark">#9e20a9</item>
+        <item name="table_row_dark_bg">#410647</item>
+        <item name="table_row_light_bg">#2c042f</item>
+    </style>
+
+    <style name="AppTheme.300" parent="AppTheme">
+        <item name="colorPrimary">#dc12dc</item>
+        <item name="colorPrimaryTransparent">#00dc12dc</item>
+        <item name="colorSecondary">#dc12dc</item>
+        <item name="colorPrimaryDark">#a01ea0</item>
+        <item name="table_row_dark_bg">#470647</item>
+        <item name="table_row_light_bg">#2f042f</item>
+    </style>
+
+    <style name="AppTheme.305" parent="AppTheme">
+        <item name="colorPrimary">#e112cf</item>
+        <item name="colorPrimaryTransparent">#00e112cf</item>
+        <item name="colorSecondary">#e112cf</item>
+        <item name="colorPrimaryDark">#a31f98</item>
+        <item name="table_row_dark_bg">#470641</item>
+        <item name="table_row_light_bg">#2f042c</item>
+    </style>
+
+    <style name="AppTheme.310" parent="AppTheme">
+        <item name="colorPrimary">#e413c1</item>
+        <item name="colorPrimaryTransparent">#00e413c1</item>
+        <item name="colorSecondary">#e413c1</item>
+        <item name="colorPrimaryDark">#a6208f</item>
+        <item name="table_row_dark_bg">#47063c</item>
+        <item name="table_row_light_bg">#2f0428</item>
+    </style>
+
+    <style name="AppTheme.315" parent="AppTheme">
+        <item name="colorPrimary">#e813b3</item>
+        <item name="colorPrimaryTransparent">#00e813b3</item>
+        <item name="colorSecondary">#e813b3</item>
+        <item name="colorPrimaryDark">#a92086</item>
+        <item name="table_row_dark_bg">#470637</item>
+        <item name="table_row_light_bg">#2f0424</item>
+    </style>
+
+    <style name="AppTheme.320" parent="AppTheme">
+        <item name="colorPrimary">#eb13a3</item>
+        <item name="colorPrimaryTransparent">#00eb13a3</item>
+        <item name="colorSecondary">#eb13a3</item>
+        <item name="colorPrimaryDark">#ab217d</item>
+        <item name="table_row_dark_bg">#470631</item>
+        <item name="table_row_light_bg">#2f0421</item>
+    </style>
+
+    <style name="AppTheme.325" parent="AppTheme">
+        <item name="colorPrimary">#ec1a95</item>
+        <item name="colorPrimaryTransparent">#00ec1a95</item>
+        <item name="colorSecondary">#ec1a95</item>
+        <item name="colorPrimaryDark">#b02275</item>
+        <item name="table_row_dark_bg">#47062c</item>
+        <item name="table_row_light_bg">#2f041d</item>
+    </style>
+
+    <style name="AppTheme.330" parent="AppTheme">
+        <item name="colorPrimary">#ed2087</item>
+        <item name="colorPrimaryTransparent">#00ed2087</item>
+        <item name="colorSecondary">#ed2087</item>
+        <item name="colorPrimaryDark">#b5226c</item>
+        <item name="table_row_dark_bg">#470626</item>
+        <item name="table_row_light_bg">#2f041a</item>
+    </style>
+
+    <style name="AppTheme.335" parent="AppTheme">
+        <item name="colorPrimary">#ed2679</item>
+        <item name="colorPrimaryTransparent">#00ed2679</item>
+        <item name="colorSecondary">#ed2679</item>
+        <item name="colorPrimaryDark">#b92362</item>
+        <item name="table_row_dark_bg">#470621</item>
+        <item name="table_row_light_bg">#2f0416</item>
+    </style>
+
+    <style name="AppTheme.340" parent="AppTheme">
+        <item name="colorPrimary">#ee2a6b</item>
+        <item name="colorPrimaryTransparent">#00ee2a6b</item>
+        <item name="colorSecondary">#ee2a6b</item>
+        <item name="colorPrimaryDark">#bc2456</item>
+        <item name="table_row_dark_bg">#47061b</item>
+        <item name="table_row_light_bg">#2f0412</item>
+    </style>
+
+    <style name="AppTheme.345" parent="AppTheme">
+        <item name="colorPrimary">#ee2d5d</item>
+        <item name="colorPrimaryTransparent">#00ee2d5d</item>
+        <item name="colorSecondary">#ee2d5d</item>
+        <item name="colorPrimaryDark">#be244b</item>
+        <item name="table_row_dark_bg">#470616</item>
+        <item name="table_row_light_bg">#2f040f</item>
+    </style>
+
+    <style name="AppTheme.350" parent="AppTheme">
+        <item name="colorPrimary">#ee2f4f</item>
+        <item name="colorPrimaryTransparent">#00ee2f4f</item>
+        <item name="colorSecondary">#ee2f4f</item>
+        <item name="colorPrimaryDark">#c0253e</item>
+        <item name="table_row_dark_bg">#470611</item>
+        <item name="table_row_light_bg">#2f040b</item>
+    </style>
+
+    <style name="AppTheme.355" parent="AppTheme">
+        <item name="colorPrimary">#ee3141</item>
+        <item name="colorPrimaryTransparent">#00ee3141</item>
+        <item name="colorSecondary">#ee3141</item>
+        <item name="colorPrimaryDark">#c12532</item>
+        <item name="table_row_dark_bg">#47060b</item>
+        <item name="table_row_light_bg">#2f0407</item>
+    </style>
+    <!-- theme list end -->
+
+    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.DayNight.ActionBar" />
+
+    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.DayNight" />
+
+    <style name="nav_button">
+        <item name="android:layout_width">match_parent</item>
+        <item name="android:layout_height">@dimen/thumb_row_height</item>
+        <item name="android:drawablePadding">@dimen/activity_horizontal_margin</item>
+        <item name="android:clickable">true</item>
+        <item name="android:focusable">auto</item>
+        <item name="android:gravity">center_vertical|start</item>
+        <item name="android:paddingStart">@dimen/activity_horizontal_margin</item>
+        <item name="android:paddingEnd">@dimen/activity_horizontal_margin</item>
+    </style>
+
+    <style name="account_summary_amounts">
+        <item name="android:textAlignment">viewEnd</item>
+    </style>
+
+    <style name="transaction_list_comment">
+        <item name="android:textAppearance">@android:style/TextAppearance.Material.Small</item>
+        <item name="android:textColor">?commentColor</item>
+    </style>
+
+    <dimen name="thumb_row_height">48dp</dimen>
+    <dimen name="default_account_row_height">64.2857sp</dimen>
+</resources>
index 25f15fb8129f69ae91e11d4301f1b64721d8fb40..cc4f07b24c5cb7d7ef444f8f8be88566bab32ae1 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -17,7 +17,4 @@
 
 <resources>
 
 
 <resources>
 
-    <style name="StretchedTextView" parent="Widget.AppCompat.TextView">
-        <item name="android:autoSizeTextType">uniform</item>
-    </style>
 </resources>
\ No newline at end of file
 </resources>
\ No newline at end of file
index 17b632966ff87c85744dcbed46f00a8f3d6bc949..ded168533667f9b0ce81cdb77b1b99dad0c40d19 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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>
         <!--<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
 </resources>
\ No newline at end of file
index afb1fc06f3ab6545ac48c6a0c8cf3865a87ed5f6..b34995970479dd7e7e4d98116631483b5d5d4d88 100644 (file)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -21,6 +21,9 @@
     <attr name="table_row_dark_bg" format="reference|color"/>
     <attr name="table_row_light_bg" format="reference|color"/>
     <attr name="textColor" format="reference|color"/>
     <attr name="table_row_dark_bg" format="reference|color"/>
     <attr name="table_row_light_bg" format="reference|color"/>
     <attr name="textColor" format="reference|color"/>
+    <attr name="commentColor" format="reference|color" />
     <attr name="colorPrimaryTransparent" format="reference|color" />
     <attr name="colorPrimaryTransparent" format="reference|color" />
-
+    <attr name="main_header_shadow_height" format="reference|dimension" />
+    <attr name="shadowStartColor" format="reference|color" />
+    <attr name="shadowEndColor" format="reference|color" />
 </resources>
\ No newline at end of file
 </resources>
\ No newline at end of file
index 89d9c379e2660e457820b7cc8d5d644ed649e561..b537aadb5c3ca3c2272717f8c4630369a3deae5b 100644 (file)
   -->
 
 <resources>
   -->
 
 <resources>
-    <color name="colorPrimary">#935FF2</color>
-    <color name="colorPrimaryDark">#3e148c</color>
-    <color name="colorAccent">#653BD0</color>
-    <color name="drawer_background">#ffffffff</color>
-    <color name="table_row_dark_bg">#286c33d4</color>
-    <color name="table_row_light_bg">#28ddcbff</color>
-    <color name="header_border">#804a148c</color>
-    <color name="error_background">#FFE1E2</color>
 </resources>
 </resources>
index d44c920a38d874e2014e71b1633db7b952d58130..5e5853575e65199b7ba37bde589b31e2ffc3f8fb 100644 (file)
@@ -1,5 +1,5 @@
 <!--
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ MoLe is free software: you can distribute it and/or modify it
   ~ under the term of the GNU General Public License as published by
@@ -19,9 +19,9 @@
     <!-- Default screen margins, per the Android Design guidelines. -->
     <dimen name="activity_horizontal_margin">16dp</dimen>
     <dimen name="activity_vertical_margin">8dp</dimen>
     <!-- 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="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="text_margin">16dp</dimen>
+    <dimen name="half_text_margin">8dp</dimen>
+    <dimen name="quarter_text_margin">4dp</dimen>
 </resources>
\ No newline at end of file
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644 (file)
index 0000000..dad99c8
--- /dev/null
@@ -0,0 +1,21 @@
+<?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/>.
+  -->
+
+<resources>
+    <color name="ic_launcher_background">#935FF2</color>
+</resources>
\ No newline at end of file
index a314e7acc06d1a926d23df02bc80e4bf6f35e349..09840becfe91f384456a4b98060f07cd32287b95 100644 (file)
@@ -18,6 +18,5 @@
 
 <resources>
     <item name="VH" type="id" />
 
 <resources>
     <item name="VH" type="id" />
-    <item name="POS" type="id" />
     <item name="SELECTING" type="id" />
 </resources>
\ No newline at end of file
     <item name="SELECTING" type="id" />
 </resources>
\ No newline at end of file
index 5f98037e2fc6d2ac888c2a4cdd63242dfe6efb7f..52d84690005411a968feef747c3b8d94bf881da9 100644 (file)
@@ -1,5 +1,5 @@
 <!--
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ 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="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="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 -->
 
     <!-- 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_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_default_backend_url" translatable="false">https://server/loc</string>
 
-    <!-- Example settings for Data & Sync -->
-    <string name="pref_title_sync_frequency">Sync frequency</string>
-    <string-array name="pref_sync_frequency_titles">
-        <item>15 minutes</item>
-        <item>30 minutes</item>
-        <item>1 hour</item>
-        <item>3 hours</item>
-        <item>6 hours</item>
-        <item>Never</item>
-    </string-array>
-    <string-array name="pref_sync_frequency_values">
-        <item>15</item>
-        <item>30</item>
-        <item>60</item>
-        <item>180</item>
-        <item>360</item>
-        <item>-1</item>
-    </string-array>
-
-    <string name="pref_title_system_sync_settings">System sync settings</string>
-
-    <!-- Example settings for Notifications -->
-    <string name="pref_header_notifications">Notifications</string>
-
-    <string name="pref_title_new_message_notifications">New message notifications</string>
-
-    <string name="pref_title_ringtone">Ringtone</string>
-    <string name="pref_ringtone_silent">Silent</string>
-
-    <string name="pref_title_vibrate">Vibrate</string>
     <string name="pref_title_backend_auth_user">Username</string>
     <string name="pref_title_backend_auth_password">Password</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="pref_title_backend_auth_user">Username</string>
     <string name="pref_title_backend_auth_password">Password</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="progress_N_accounts_loaded">%d accounts loaded</string>
     <string name="new_transaction_description_hint">Description</string>
     <string name="account_summary_title">Accounts</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="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="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 name="profile_name_label">Profile name</string>
     <string name="url_label">URL</string>
     <string name="text_welcome">Welcome</string>
         <item>December</item>
     </string-array>
     <string name="posting_permitted">Posting of new transactions enabled</string>
         <item>December</item>
     </string-array>
     <string name="posting_permitted">Posting of new transactions enabled</string>
-    <string name="profile_subitlte_read_only">(Read only)</string>
-    <string name="crash_report_contents_label">Crash report contents:</string>
-    <string name="crash_dialog_title">MoLe chashed</string>
+    <string name="profile_subtitle_read_only">(Read only)</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_app_label">Crash app</string>
     <string name="btn_send_crash_report">Send…</string>
     <string name="btn_not_now">Not now</string>
     <string name="crash_app_label">Crash app</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="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="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="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="zero_amount">0.00</string>
     <string name="new_transaction_saving">Saving…</string>
     <string name="simulate_save_label">Simulate save requests</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="zero_amount">0.00</string>
     <string name="new_transaction_saving">Saving…</string>
     <string name="simulate_save_label">Simulate save requests</string>
-    <string name="simulate_save_condensed_label">Simul. save</string>
+    <string name="simulate_save_condensed_label">Simulate saves</string>
     <string name="simulation_label">SIMULATION</string>
     <string name="profile_future_dates_label">Allow input of dates in the future</string>
     <string name="future_dates_none">No future dates are allowed</string>
     <string name="simulation_label">SIMULATION</string>
     <string name="profile_future_dates_label">Allow input of dates in the future</string>
     <string name="future_dates_none">No future dates are allowed</string>
+    <string name="future_dates_7">Up to a week</string>
+    <string name="future_dates_14">Up to two weeks</string>
     <string name="future_dates_30">Up to a month</string>
     <string name="future_dates_60">Up to two months</string>
     <string name="future_dates_90">Up to three months</string>
     <string name="future_dates_180">Up to six months</string>
     <string name="future_dates_365">Up to a year</string>
     <string name="future_dates_all">Without restrictions</string>
     <string name="future_dates_30">Up to a month</string>
     <string name="future_dates_60">Up to two months</string>
     <string name="future_dates_90">Up to three months</string>
     <string name="future_dates_180">Up to six months</string>
     <string name="future_dates_365">Up to a year</string>
     <string name="future_dates_all">Without restrictions</string>
-    <string name="api_html">Simulate HTML form</string>
-    <string name="api_pre_1_15">version before 1.15</string>
-    <string name="api_post_1_14">version 1.15 and above</string>
-    <string name="api_auto">Detect automaticaly</string>
+    <string name="api_html">Version before 1.14</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="transaction_account_comment_hint">comment</string>
+    <string name="choose_currency_label">Currency</string>
+    <string name="new_currency_name_hint">currency/commodity</string>
+    <string name="btn_no_currency">none</string>
+    <string name="currency_position_left">Left</string>
+    <string name="currency_position_right">Right</string>
+    <string name="currency_has_gap">Offset from the value</string>
+    <string name="show_currency_input">Currency</string>
+    <string name="currency_input_by_default">Commodity input visible by default</string>
+    <string name="profile_default_commodity">Default commodity</string>
+    <string name="ignoring_preferred_account">No transactions with preferred account found</string>
+    <string name="icon">icon</string>
+    <string name="show_comments_switch">Comments</string>
+    <string name="show_comment_input_by_default">Show comment fields by default</string>
+    <string name="filter_menu_title">Filter</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">%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>
 </resources>
index f9be6d321ae9775ace916f3832668ae853679e7d..f6467892b637fe664c9b14990e10839e1d2df286 100644 (file)
@@ -1,5 +1,5 @@
 <!--
 <!--
-  ~ 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
   ~ This file is part of MoLe.
   ~ 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>
   -->
 
 <resources>
-
     <!-- Base application theme. -->
     <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
-        <item name="colorPrimary">#935FF2</item>
-        <item name="colorAccent">#653BD0</item>
-        <item name="drawer_background">#ffffffff</item>
-        <item name="table_row_dark_bg">#286c33d4</item>
-        <item name="table_row_light_bg">#28ddcbff</item>
-        <item name="textColor">@android:color/tab_indicator_text</item>
-    </style>
-
-    <style name="StretchedTextView" parent="Widget.AppCompat.TextView"></style>
     <!-- base hue: 261.2245° -->
     <!-- target primary color: #935FF2 -->
     <!-- base hue: 261.2245° -->
     <!-- target primary color: #935FF2 -->
-    <!-- theme list start -->
-    <style name="AppTheme.NoActionBar">
+    <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="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
         <item name="windowActionBar">false</item>
         <item name="windowNoTitle">true</item>
-        <item name="textColor">#8a000000</item>
+        <item name="textColor">#686868</item>
+        <item name="commentColor">#a0a0a0</item>
+        <item name="colorOnSecondary">@android:color/white</item>
+        <item name="textInputStyle">
+            @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense
+        </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="colorPrimaryDark">#6920ed</item>
+        <item name="table_row_dark_bg">#efe7fd</item>
+        <item name="table_row_light_bg">#f9f6fe</item>
+        <item name="background">@null</item>
+        <item name="main_header_shadow_height">4dp</item>
+        <item name="shadowStartColor">#80000000</item>
+        <item name="shadowEndColor">#00000000</item>
+    </style>
+
+    <!-- theme list start -->
+
+    <style name="AppTheme.default" parent="AppTheme">
         <item name="colorPrimary">#935ff2</item>
         <item name="colorPrimaryTransparent">#00935ff2</item>
         <item name="colorPrimary">#935ff2</item>
         <item name="colorPrimaryTransparent">#00935ff2</item>
-        <item name="colorAccent">#5f14e9</item>
-        <item name="drawer_background">#ffffffff</item>
+        <item name="colorSecondary">#935ff2</item>
+        <item name="colorPrimaryDark">#6f35d8</item>
         <item name="table_row_dark_bg">#efe7fd</item>
         <item name="table_row_light_bg">#f9f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#efe7fd</item>
         <item name="table_row_light_bg">#f9f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.000" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ec1717</item>
-        <item name="colorPrimaryTransparent">#00ec1717</item>
-        <item name="colorAccent">#b00f0f</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.000" parent="AppTheme">
+        <item name="colorPrimary">#ee3232</item>
+        <item name="colorPrimaryTransparent">#00ee3232</item>
+        <item name="colorSecondary">#ee3232</item>
+        <item name="colorPrimaryDark">#c22525</item>
         <item name="table_row_dark_bg">#fde7e7</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7e7</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.005" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#e92514</item>
-        <item name="colorPrimaryTransparent">#00e92514</item>
-        <item name="colorAccent">#ac1c0e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.005" parent="AppTheme">
+        <item name="colorPrimary">#ed3625</item>
+        <item name="colorPrimaryTransparent">#00ed3625</item>
+        <item name="colorSecondary">#ed3625</item>
+        <item name="colorPrimaryDark">#b83023</item>
         <item name="table_row_dark_bg">#fde9e7</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fde9e7</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.010" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#e33613</item>
-        <item name="colorPrimaryTransparent">#00e33613</item>
-        <item name="colorAccent">#a7280e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.010" parent="AppTheme">
+        <item name="colorPrimary">#ec3915</item>
+        <item name="colorPrimaryTransparent">#00ec3915</item>
+        <item name="colorSecondary">#ec3915</item>
+        <item name="colorPrimaryDark">#ad3821</item>
         <item name="table_row_dark_bg">#fdebe7</item>
         <item name="table_row_light_bg">#fef7f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdebe7</item>
         <item name="table_row_light_bg">#fef7f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.015" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#dd4513</item>
-        <item name="colorPrimaryTransparent">#00dd4513</item>
-        <item name="colorAccent">#a3330e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.015" parent="AppTheme">
+        <item name="colorPrimary">#e24612</item>
+        <item name="colorPrimaryTransparent">#00e24612</item>
+        <item name="colorSecondary">#e24612</item>
+        <item name="colorPrimaryDark">#a4411f</item>
         <item name="table_row_dark_bg">#fdede7</item>
         <item name="table_row_light_bg">#fef8f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdede7</item>
         <item name="table_row_light_bg">#fef8f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.020" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#d75412</item>
-        <item name="colorPrimaryTransparent">#00d75412</item>
-        <item name="colorAccent">#9e3d0d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.020" parent="AppTheme">
+        <item name="colorPrimary">#d75311</item>
+        <item name="colorPrimaryTransparent">#00d75311</item>
+        <item name="colorSecondary">#d75311</item>
+        <item name="colorPrimaryDark">#9c481e</item>
         <item name="table_row_dark_bg">#fdefe7</item>
         <item name="table_row_light_bg">#fef8f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdefe7</item>
         <item name="table_row_light_bg">#fef8f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.025" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#d16112</item>
-        <item name="colorPrimaryTransparent">#00d16112</item>
-        <item name="colorAccent">#99470d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.025" parent="AppTheme">
+        <item name="colorPrimary">#cb5e10</item>
+        <item name="colorPrimaryTransparent">#00cb5e10</item>
+        <item name="colorSecondary">#cb5e10</item>
+        <item name="colorPrimaryDark">#934e1c</item>
         <item name="table_row_dark_bg">#fdf0e7</item>
         <item name="table_row_light_bg">#fef9f6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf0e7</item>
         <item name="table_row_light_bg">#fef9f6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.030" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#cb6e11</item>
-        <item name="colorPrimaryTransparent">#00cb6e11</item>
-        <item name="colorAccent">#94500c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.030" parent="AppTheme">
+        <item name="colorPrimary">#bf670f</item>
+        <item name="colorPrimaryTransparent">#00bf670f</item>
+        <item name="colorSecondary">#bf670f</item>
+        <item name="colorPrimaryDark">#8a521a</item>
         <item name="table_row_dark_bg">#fdf2e7</item>
         <item name="table_row_light_bg">#fefaf6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf2e7</item>
         <item name="table_row_light_bg">#fefaf6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.035" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#c57a11</item>
-        <item name="colorPrimaryTransparent">#00c57a11</item>
-        <item name="colorAccent">#8f580c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.035" parent="AppTheme">
+        <item name="colorPrimary">#b26e0e</item>
+        <item name="colorPrimaryTransparent">#00b26e0e</item>
+        <item name="colorSecondary">#b26e0e</item>
+        <item name="colorPrimaryDark">#825619</item>
         <item name="table_row_dark_bg">#fdf4e7</item>
         <item name="table_row_light_bg">#fefbf6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf4e7</item>
         <item name="table_row_light_bg">#fefbf6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.040" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#be8410</item>
-        <item name="colorPrimaryTransparent">#00be8410</item>
-        <item name="colorAccent">#8a600c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.040" parent="AppTheme">
+        <item name="colorPrimary">#a7740e</item>
+        <item name="colorPrimaryTransparent">#00a7740e</item>
+        <item name="colorSecondary">#a7740e</item>
+        <item name="colorPrimaryDark">#795917</item>
         <item name="table_row_dark_bg">#fdf6e7</item>
         <item name="table_row_light_bg">#fefbf6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf6e7</item>
         <item name="table_row_light_bg">#fefbf6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.045" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#b88e0f</item>
-        <item name="colorPrimaryTransparent">#00b88e0f</item>
-        <item name="colorAccent">#85670b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.045" parent="AppTheme">
+        <item name="colorPrimary">#9d790d</item>
+        <item name="colorPrimaryTransparent">#009d790d</item>
+        <item name="colorSecondary">#9d790d</item>
+        <item name="colorPrimaryDark">#725b16</item>
         <item name="table_row_dark_bg">#fdf8e7</item>
         <item name="table_row_light_bg">#fefcf6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf8e7</item>
         <item name="table_row_light_bg">#fefcf6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.050" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#b2960f</item>
-        <item name="colorPrimaryTransparent">#00b2960f</item>
-        <item name="colorAccent">#806c0b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.050" parent="AppTheme">
+        <item name="colorPrimary">#937d0c</item>
+        <item name="colorPrimaryTransparent">#00937d0c</item>
+        <item name="colorSecondary">#937d0c</item>
+        <item name="colorPrimaryDark">#6b5c14</item>
         <item name="table_row_dark_bg">#fdf9e7</item>
         <item name="table_row_light_bg">#fefdf6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdf9e7</item>
         <item name="table_row_light_bg">#fefdf6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.055" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ab9e0e</item>
-        <item name="colorPrimaryTransparent">#00ab9e0e</item>
-        <item name="colorAccent">#7b710a</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.055" parent="AppTheme">
+        <item name="colorPrimary">#8b800b</item>
+        <item name="colorPrimaryTransparent">#008b800b</item>
+        <item name="colorSecondary">#8b800b</item>
+        <item name="colorPrimaryDark">#655e13</item>
         <item name="table_row_dark_bg">#fdfbe7</item>
         <item name="table_row_dark_bg">#fdfbe7</item>
-        <item name="table_row_light_bg">#fefdf6</item>
+        <item name="table_row_light_bg">#fefef6</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.060" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#a5a50e</item>
-        <item name="colorPrimaryTransparent">#00a5a50e</item>
-        <item name="colorAccent">#76760a</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.060" parent="AppTheme">
+        <item name="colorPrimary">#82820b</item>
+        <item name="colorPrimaryTransparent">#0082820b</item>
+        <item name="colorSecondary">#82820b</item>
+        <item name="colorPrimaryDark">#5f5f12</item>
         <item name="table_row_dark_bg">#fdfde7</item>
         <item name="table_row_light_bg">#fefef6</item>
     </style>
 
         <item name="table_row_dark_bg">#fdfde7</item>
         <item name="table_row_light_bg">#fefef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.065" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#9eab0e</item>
-        <item name="colorPrimaryTransparent">#009eab0e</item>
-        <item name="colorAccent">#717b0a</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.065" parent="AppTheme">
+        <item name="colorPrimary">#7a840b</item>
+        <item name="colorPrimaryTransparent">#007a840b</item>
+        <item name="colorSecondary">#7a840b</item>
+        <item name="colorPrimaryDark">#596012</item>
         <item name="table_row_dark_bg">#fbfde7</item>
         <item name="table_row_dark_bg">#fbfde7</item>
-        <item name="table_row_light_bg">#fdfef6</item>
+        <item name="table_row_light_bg">#fefef6</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.070" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#96b20f</item>
-        <item name="colorPrimaryTransparent">#0096b20f</item>
-        <item name="colorAccent">#6c800b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.070" parent="AppTheme">
+        <item name="colorPrimary">#72870b</item>
+        <item name="colorPrimaryTransparent">#0072870b</item>
+        <item name="colorSecondary">#72870b</item>
+        <item name="colorPrimaryDark">#556213</item>
         <item name="table_row_dark_bg">#f9fde7</item>
         <item name="table_row_light_bg">#fdfef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f9fde7</item>
         <item name="table_row_light_bg">#fdfef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.075" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#8eb80f</item>
-        <item name="colorPrimaryTransparent">#008eb80f</item>
-        <item name="colorAccent">#67850b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.075" parent="AppTheme">
+        <item name="colorPrimary">#69890b</item>
+        <item name="colorPrimaryTransparent">#0069890b</item>
+        <item name="colorSecondary">#69890b</item>
+        <item name="colorPrimaryDark">#4f6313</item>
         <item name="table_row_dark_bg">#f8fde7</item>
         <item name="table_row_light_bg">#fcfef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f8fde7</item>
         <item name="table_row_light_bg">#fcfef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.080" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#84be10</item>
-        <item name="colorPrimaryTransparent">#0084be10</item>
-        <item name="colorAccent">#608a0c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.080" parent="AppTheme">
+        <item name="colorPrimary">#608b0b</item>
+        <item name="colorPrimaryTransparent">#00608b0b</item>
+        <item name="colorSecondary">#608b0b</item>
+        <item name="colorPrimaryDark">#4a6513</item>
         <item name="table_row_dark_bg">#f6fde7</item>
         <item name="table_row_light_bg">#fbfef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f6fde7</item>
         <item name="table_row_light_bg">#fbfef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.085" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#7ac511</item>
-        <item name="colorPrimaryTransparent">#007ac511</item>
-        <item name="colorAccent">#588f0c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.085" parent="AppTheme">
+        <item name="colorPrimary">#568c0b</item>
+        <item name="colorPrimaryTransparent">#00568c0b</item>
+        <item name="colorSecondary">#568c0b</item>
+        <item name="colorPrimaryDark">#436513</item>
         <item name="table_row_dark_bg">#f4fde7</item>
         <item name="table_row_light_bg">#fbfef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f4fde7</item>
         <item name="table_row_light_bg">#fbfef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.090" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#6ecb11</item>
-        <item name="colorPrimaryTransparent">#006ecb11</item>
-        <item name="colorAccent">#50940c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.090" parent="AppTheme">
+        <item name="colorPrimary">#4d8e0b</item>
+        <item name="colorPrimaryTransparent">#004d8e0b</item>
+        <item name="colorSecondary">#4d8e0b</item>
+        <item name="colorPrimaryDark">#3d6714</item>
         <item name="table_row_dark_bg">#f2fde7</item>
         <item name="table_row_light_bg">#fafef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f2fde7</item>
         <item name="table_row_light_bg">#fafef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.095" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#61d112</item>
-        <item name="colorPrimaryTransparent">#0061d112</item>
-        <item name="colorAccent">#47990d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.095" parent="AppTheme">
+        <item name="colorPrimary">#428e0c</item>
+        <item name="colorPrimaryTransparent">#00428e0c</item>
+        <item name="colorSecondary">#428e0c</item>
+        <item name="colorPrimaryDark">#376714</item>
         <item name="table_row_dark_bg">#f0fde7</item>
         <item name="table_row_light_bg">#f9fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#f0fde7</item>
         <item name="table_row_light_bg">#f9fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.100" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#54d712</item>
-        <item name="colorPrimaryTransparent">#0054d712</item>
-        <item name="colorAccent">#3d9e0d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.100" parent="AppTheme">
+        <item name="colorPrimary">#38900c</item>
+        <item name="colorPrimaryTransparent">#0038900c</item>
+        <item name="colorSecondary">#38900c</item>
+        <item name="colorPrimaryDark">#306914</item>
         <item name="table_row_dark_bg">#effde7</item>
         <item name="table_row_light_bg">#f8fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#effde7</item>
         <item name="table_row_light_bg">#f8fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.105" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#45dd13</item>
-        <item name="colorPrimaryTransparent">#0045dd13</item>
-        <item name="colorAccent">#33a30e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.105" parent="AppTheme">
+        <item name="colorPrimary">#2d910c</item>
+        <item name="colorPrimaryTransparent">#002d910c</item>
+        <item name="colorSecondary">#2d910c</item>
+        <item name="colorPrimaryDark">#296a14</item>
         <item name="table_row_dark_bg">#edfde7</item>
         <item name="table_row_light_bg">#f8fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#edfde7</item>
         <item name="table_row_light_bg">#f8fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.110" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#36e313</item>
-        <item name="colorPrimaryTransparent">#0036e313</item>
-        <item name="colorAccent">#28a70e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.110" parent="AppTheme">
+        <item name="colorPrimary">#22910c</item>
+        <item name="colorPrimaryTransparent">#0022910c</item>
+        <item name="colorSecondary">#22910c</item>
+        <item name="colorPrimaryDark">#226a14</item>
         <item name="table_row_dark_bg">#ebfde7</item>
         <item name="table_row_light_bg">#f7fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#ebfde7</item>
         <item name="table_row_light_bg">#f7fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.115" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#25e914</item>
-        <item name="colorPrimaryTransparent">#0025e914</item>
-        <item name="colorAccent">#1cac0e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.115" parent="AppTheme">
+        <item name="colorPrimary">#17920c</item>
+        <item name="colorPrimaryTransparent">#0017920c</item>
+        <item name="colorSecondary">#17920c</item>
+        <item name="colorPrimaryDark">#1b6a14</item>
         <item name="table_row_dark_bg">#e9fde7</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#e9fde7</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.120" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#17ec17</item>
-        <item name="colorPrimaryTransparent">#0017ec17</item>
-        <item name="colorAccent">#0fb00f</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.120" parent="AppTheme">
+        <item name="colorPrimary">#0c920c</item>
+        <item name="colorPrimaryTransparent">#000c920c</item>
+        <item name="colorSecondary">#0c920c</item>
+        <item name="colorPrimaryDark">#146a14</item>
         <item name="table_row_dark_bg">#e7fde7</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fde7</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.125" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#1dec2e</item>
-        <item name="colorPrimaryTransparent">#001dec2e</item>
-        <item name="colorAccent">#0fb51d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.125" parent="AppTheme">
+        <item name="colorPrimary">#0c9217</item>
+        <item name="colorPrimaryTransparent">#000c9217</item>
+        <item name="colorSecondary">#0c9217</item>
+        <item name="colorPrimaryDark">#146a1b</item>
         <item name="table_row_dark_bg">#e7fde9</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fde9</item>
         <item name="table_row_light_bg">#f6fef6</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.130" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#22ec44</item>
-        <item name="colorPrimaryTransparent">#0022ec44</item>
-        <item name="colorAccent">#10b92c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.130" parent="AppTheme">
+        <item name="colorPrimary">#0c9222</item>
+        <item name="colorPrimaryTransparent">#000c9222</item>
+        <item name="colorSecondary">#0c9222</item>
+        <item name="colorPrimaryDark">#146a23</item>
         <item name="table_row_dark_bg">#e7fdeb</item>
         <item name="table_row_light_bg">#f6fef7</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdeb</item>
         <item name="table_row_light_bg">#f6fef7</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.135" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#27ed59</item>
-        <item name="colorPrimaryTransparent">#0027ed59</item>
-        <item name="colorAccent">#10bd3b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.135" parent="AppTheme">
+        <item name="colorPrimary">#0c922d</item>
+        <item name="colorPrimaryTransparent">#000c922d</item>
+        <item name="colorSecondary">#0c922d</item>
+        <item name="colorPrimaryDark">#146a2a</item>
         <item name="table_row_dark_bg">#e7fded</item>
         <item name="table_row_light_bg">#f6fef8</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fded</item>
         <item name="table_row_light_bg">#f6fef8</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.140" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#2ced6d</item>
-        <item name="colorPrimaryTransparent">#002ced6d</item>
-        <item name="colorAccent">#10c14b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.140" parent="AppTheme">
+        <item name="colorPrimary">#0c9138</item>
+        <item name="colorPrimaryTransparent">#000c9138</item>
+        <item name="colorSecondary">#0c9138</item>
+        <item name="colorPrimaryDark">#146a31</item>
         <item name="table_row_dark_bg">#e7fdef</item>
         <item name="table_row_light_bg">#f6fef8</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdef</item>
         <item name="table_row_light_bg">#f6fef8</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.145" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#31ee80</item>
-        <item name="colorPrimaryTransparent">#0031ee80</item>
-        <item name="colorAccent">#11c55c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.145" parent="AppTheme">
+        <item name="colorPrimary">#0c9143</item>
+        <item name="colorPrimaryTransparent">#000c9143</item>
+        <item name="colorSecondary">#0c9143</item>
+        <item name="colorPrimaryDark">#146a38</item>
         <item name="table_row_dark_bg">#e7fdf0</item>
         <item name="table_row_light_bg">#f6fef9</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf0</item>
         <item name="table_row_light_bg">#f6fef9</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.150" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#36ee92</item>
-        <item name="colorPrimaryTransparent">#0036ee92</item>
-        <item name="colorAccent">#11c96d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.150" parent="AppTheme">
+        <item name="colorPrimary">#0c904e</item>
+        <item name="colorPrimaryTransparent">#000c904e</item>
+        <item name="colorSecondary">#0c904e</item>
+        <item name="colorPrimaryDark">#14693e</item>
         <item name="table_row_dark_bg">#e7fdf2</item>
         <item name="table_row_light_bg">#f6fefa</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf2</item>
         <item name="table_row_light_bg">#f6fefa</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.155" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#3aeea3</item>
-        <item name="colorPrimaryTransparent">#003aeea3</item>
-        <item name="colorAccent">#11cc7e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.155" parent="AppTheme">
+        <item name="colorPrimary">#0c9059</item>
+        <item name="colorPrimaryTransparent">#000c9059</item>
+        <item name="colorSecondary">#0c9059</item>
+        <item name="colorPrimaryDark">#146945</item>
         <item name="table_row_dark_bg">#e7fdf4</item>
         <item name="table_row_light_bg">#f6fefb</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf4</item>
         <item name="table_row_light_bg">#f6fefb</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.160" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#3fefb4</item>
-        <item name="colorPrimaryTransparent">#003fefb4</item>
-        <item name="colorAccent">#11d090</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.160" parent="AppTheme">
+        <item name="colorPrimary">#0c8f63</item>
+        <item name="colorPrimaryTransparent">#000c8f63</item>
+        <item name="colorSecondary">#0c8f63</item>
+        <item name="colorPrimaryDark">#14684c</item>
         <item name="table_row_dark_bg">#e7fdf6</item>
         <item name="table_row_light_bg">#f6fefb</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf6</item>
         <item name="table_row_light_bg">#f6fefb</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.165" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#43efc4</item>
-        <item name="colorPrimaryTransparent">#0043efc4</item>
-        <item name="colorAccent">#12d3a3</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.165" parent="AppTheme">
+        <item name="colorPrimary">#0c8e6e</item>
+        <item name="colorPrimaryTransparent">#000c8e6e</item>
+        <item name="colorSecondary">#0c8e6e</item>
+        <item name="colorPrimaryDark">#146753</item>
         <item name="table_row_dark_bg">#e7fdf8</item>
         <item name="table_row_light_bg">#f6fefc</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf8</item>
         <item name="table_row_light_bg">#f6fefc</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.170" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#47f0d3</item>
-        <item name="colorPrimaryTransparent">#0047f0d3</item>
-        <item name="colorAccent">#12d6b5</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.170" parent="AppTheme">
+        <item name="colorPrimary">#0c8e79</item>
+        <item name="colorPrimaryTransparent">#000c8e79</item>
+        <item name="colorSecondary">#0c8e79</item>
+        <item name="colorPrimaryDark">#14675a</item>
         <item name="table_row_dark_bg">#e7fdf9</item>
         <item name="table_row_light_bg">#f6fefd</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdf9</item>
         <item name="table_row_light_bg">#f6fefd</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.175" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#4af0e2</item>
-        <item name="colorPrimaryTransparent">#004af0e2</item>
-        <item name="colorAccent">#12d9c8</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.175" parent="AppTheme">
+        <item name="colorPrimary">#0b8e83</item>
+        <item name="colorPrimaryTransparent">#000b8e83</item>
+        <item name="colorSecondary">#0b8e83</item>
+        <item name="colorPrimaryDark">#146760</item>
         <item name="table_row_dark_bg">#e7fdfb</item>
         <item name="table_row_dark_bg">#e7fdfb</item>
-        <item name="table_row_light_bg">#f6fefd</item>
+        <item name="table_row_light_bg">#f6fefe</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.180" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#4ef0f0</item>
-        <item name="colorPrimaryTransparent">#004ef0f0</item>
-        <item name="colorAccent">#12dbdb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.180" parent="AppTheme">
+        <item name="colorPrimary">#0b8d8d</item>
+        <item name="colorPrimaryTransparent">#000b8d8d</item>
+        <item name="colorSecondary">#0b8d8d</item>
+        <item name="colorPrimaryDark">#136666</item>
         <item name="table_row_dark_bg">#e7fdfd</item>
         <item name="table_row_light_bg">#f6fefe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7fdfd</item>
         <item name="table_row_light_bg">#f6fefe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.185" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#51e3f0</item>
-        <item name="colorPrimaryTransparent">#0051e3f0</item>
-        <item name="colorAccent">#13cdde</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.185" parent="AppTheme">
+        <item name="colorPrimary">#0c8b97</item>
+        <item name="colorPrimaryTransparent">#000c8b97</item>
+        <item name="colorSecondary">#0c8b97</item>
+        <item name="colorPrimaryDark">#15666e</item>
         <item name="table_row_dark_bg">#e7fbfd</item>
         <item name="table_row_dark_bg">#e7fbfd</item>
-        <item name="table_row_light_bg">#f6fdfe</item>
+        <item name="table_row_light_bg">#f6fefe</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.190" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#54d6f1</item>
-        <item name="colorPrimaryTransparent">#0054d6f1</item>
-        <item name="colorAccent">#13bee0</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.190" parent="AppTheme">
+        <item name="colorPrimary">#0d89a2</item>
+        <item name="colorPrimaryTransparent">#000d89a2</item>
+        <item name="colorSecondary">#0d89a2</item>
+        <item name="colorPrimaryDark">#166676</item>
         <item name="table_row_dark_bg">#e7f9fd</item>
         <item name="table_row_light_bg">#f6fdfe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f9fd</item>
         <item name="table_row_light_bg">#f6fdfe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.195" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#56caf1</item>
-        <item name="colorPrimaryTransparent">#0056caf1</item>
-        <item name="colorAccent">#13aee2</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.195" parent="AppTheme">
+        <item name="colorPrimary">#0e88b0</item>
+        <item name="colorPrimaryTransparent">#000e88b0</item>
+        <item name="colorSecondary">#0e88b0</item>
+        <item name="colorPrimaryDark">#186680</item>
         <item name="table_row_dark_bg">#e7f8fd</item>
         <item name="table_row_light_bg">#f6fcfe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f8fd</item>
         <item name="table_row_light_bg">#f6fcfe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.200" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#58bef1</item>
-        <item name="colorPrimaryTransparent">#0058bef1</item>
-        <item name="colorAccent">#139ee4</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.200" parent="AppTheme">
+        <item name="colorPrimary">#1086c0</item>
+        <item name="colorPrimaryTransparent">#001086c0</item>
+        <item name="colorSecondary">#1086c0</item>
+        <item name="colorPrimaryDark">#1b668c</item>
         <item name="table_row_dark_bg">#e7f6fd</item>
         <item name="table_row_light_bg">#f6fbfe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f6fd</item>
         <item name="table_row_light_bg">#f6fbfe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.205" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#5bb2f1</item>
-        <item name="colorPrimaryTransparent">#005bb2f1</item>
-        <item name="colorAccent">#138ee6</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.205" parent="AppTheme">
+        <item name="colorPrimary">#1182d2</item>
+        <item name="colorPrimaryTransparent">#001182d2</item>
+        <item name="colorSecondary">#1182d2</item>
+        <item name="colorPrimaryDark">#1d6599</item>
         <item name="table_row_dark_bg">#e7f4fd</item>
         <item name="table_row_light_bg">#f6fbfe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f4fd</item>
         <item name="table_row_light_bg">#f6fbfe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.210" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#5ca7f1</item>
-        <item name="colorPrimaryTransparent">#005ca7f1</item>
-        <item name="colorAccent">#137de7</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.210" parent="AppTheme">
+        <item name="colorPrimary">#137de8</item>
+        <item name="colorPrimaryTransparent">#00137de8</item>
+        <item name="colorSecondary">#137de8</item>
+        <item name="colorPrimaryDark">#2064a9</item>
         <item name="table_row_dark_bg">#e7f2fd</item>
         <item name="table_row_light_bg">#f6fafe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f2fd</item>
         <item name="table_row_light_bg">#f6fafe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.215" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#5e9bf1</item>
-        <item name="colorPrimaryTransparent">#005e9bf1</item>
-        <item name="colorAccent">#146ce8</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.215" parent="AppTheme">
+        <item name="colorPrimary">#297bee</item>
+        <item name="colorPrimaryTransparent">#00297bee</item>
+        <item name="colorSecondary">#297bee</item>
+        <item name="colorPrimaryDark">#2463bb</item>
         <item name="table_row_dark_bg">#e7f0fd</item>
         <item name="table_row_light_bg">#f6f9fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7f0fd</item>
         <item name="table_row_light_bg">#f6f9fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.220" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#5f90f2</item>
-        <item name="colorPrimaryTransparent">#005f90f2</item>
-        <item name="colorAccent">#145be9</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.220" parent="AppTheme">
+        <item name="colorPrimary">#3d78ef</item>
+        <item name="colorPrimaryTransparent">#003d78ef</item>
+        <item name="colorSecondary">#3d78ef</item>
+        <item name="colorPrimaryDark">#265dc9</item>
         <item name="table_row_dark_bg">#e7effd</item>
         <item name="table_row_light_bg">#f6f8fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7effd</item>
         <item name="table_row_light_bg">#f6f8fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.225" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#6085f2</item>
-        <item name="colorPrimaryTransparent">#006085f2</item>
-        <item name="colorAccent">#1449ea</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.225" parent="AppTheme">
+        <item name="colorPrimary">#4d76f1</item>
+        <item name="colorPrimaryTransparent">#004d76f1</item>
+        <item name="colorSecondary">#4d76f1</item>
+        <item name="colorPrimaryDark">#2954d5</item>
         <item name="table_row_dark_bg">#e7edfd</item>
         <item name="table_row_light_bg">#f6f8fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7edfd</item>
         <item name="table_row_light_bg">#f6f8fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.230" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#6179f2</item>
-        <item name="colorPrimaryTransparent">#006179f2</item>
-        <item name="colorAccent">#1438eb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.230" parent="AppTheme">
+        <item name="colorPrimary">#5a73f2</item>
+        <item name="colorPrimaryTransparent">#005a73f2</item>
+        <item name="colorSecondary">#5a73f2</item>
+        <item name="colorPrimaryDark">#314dd8</item>
         <item name="table_row_dark_bg">#e7ebfd</item>
         <item name="table_row_light_bg">#f6f7fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7ebfd</item>
         <item name="table_row_light_bg">#f6f7fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.235" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#616df2</item>
-        <item name="colorPrimaryTransparent">#00616df2</item>
-        <item name="colorAccent">#1426eb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.235" parent="AppTheme">
+        <item name="colorPrimary">#6470f2</item>
+        <item name="colorPrimaryTransparent">#006470f2</item>
+        <item name="colorSecondary">#6470f2</item>
+        <item name="colorPrimaryDark">#3946d9</item>
         <item name="table_row_dark_bg">#e7e9fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7e9fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.240" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#6161f2</item>
-        <item name="colorPrimaryTransparent">#006161f2</item>
-        <item name="colorAccent">#1414eb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.240" parent="AppTheme">
+        <item name="colorPrimary">#6e6ef3</item>
+        <item name="colorPrimaryTransparent">#006e6ef3</item>
+        <item name="colorSecondary">#6e6ef3</item>
+        <item name="colorPrimaryDark">#4040db</item>
         <item name="table_row_dark_bg">#e7e7fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e7e7fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.245" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#6d61f2</item>
-        <item name="colorPrimaryTransparent">#006d61f2</item>
-        <item name="colorAccent">#2614eb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.245" parent="AppTheme">
+        <item name="colorPrimary">#766bf3</item>
+        <item name="colorPrimaryTransparent">#00766bf3</item>
+        <item name="colorSecondary">#766bf3</item>
+        <item name="colorPrimaryDark">#4b3eda</item>
         <item name="table_row_dark_bg">#e9e7fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#e9e7fd</item>
         <item name="table_row_light_bg">#f6f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.250" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#7961f2</item>
-        <item name="colorPrimaryTransparent">#007961f2</item>
-        <item name="colorAccent">#3814eb</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.250" parent="AppTheme">
+        <item name="colorPrimary">#7f68f3</item>
+        <item name="colorPrimaryTransparent">#007f68f3</item>
+        <item name="colorSecondary">#7f68f3</item>
+        <item name="colorPrimaryDark">#563cda</item>
         <item name="table_row_dark_bg">#ebe7fd</item>
         <item name="table_row_light_bg">#f7f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#ebe7fd</item>
         <item name="table_row_light_bg">#f7f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.255" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#8560f2</item>
-        <item name="colorPrimaryTransparent">#008560f2</item>
-        <item name="colorAccent">#4914ea</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.255" parent="AppTheme">
+        <item name="colorPrimary">#8864f2</item>
+        <item name="colorPrimaryTransparent">#008864f2</item>
+        <item name="colorSecondary">#8864f2</item>
+        <item name="colorPrimaryDark">#6139d9</item>
         <item name="table_row_dark_bg">#ede7fd</item>
         <item name="table_row_light_bg">#f8f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#ede7fd</item>
         <item name="table_row_light_bg">#f8f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.260" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#905ff2</item>
-        <item name="colorPrimaryTransparent">#00905ff2</item>
-        <item name="colorAccent">#5b14e9</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.260" parent="AppTheme">
+        <item name="colorPrimary">#9161f2</item>
+        <item name="colorPrimaryTransparent">#009161f2</item>
+        <item name="colorSecondary">#9161f2</item>
+        <item name="colorPrimaryDark">#6c36d9</item>
         <item name="table_row_dark_bg">#efe7fd</item>
         <item name="table_row_light_bg">#f8f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#efe7fd</item>
         <item name="table_row_light_bg">#f8f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.265" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#9b5ef1</item>
-        <item name="colorPrimaryTransparent">#009b5ef1</item>
-        <item name="colorAccent">#6c14e8</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.265" parent="AppTheme">
+        <item name="colorPrimary">#9a5bf2</item>
+        <item name="colorPrimaryTransparent">#009a5bf2</item>
+        <item name="colorSecondary">#9a5bf2</item>
+        <item name="colorPrimaryDark">#7732d8</item>
         <item name="table_row_dark_bg">#f0e7fd</item>
         <item name="table_row_light_bg">#f9f6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f0e7fd</item>
         <item name="table_row_light_bg">#f9f6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.270" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#a75cf1</item>
-        <item name="colorPrimaryTransparent">#00a75cf1</item>
-        <item name="colorAccent">#7d13e7</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.270" parent="AppTheme">
+        <item name="colorPrimary">#a355f1</item>
+        <item name="colorPrimaryTransparent">#00a355f1</item>
+        <item name="colorSecondary">#a355f1</item>
+        <item name="colorPrimaryDark">#832ed7</item>
         <item name="table_row_dark_bg">#f2e7fd</item>
         <item name="table_row_light_bg">#faf6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f2e7fd</item>
         <item name="table_row_light_bg">#faf6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.275" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#b25bf1</item>
-        <item name="colorPrimaryTransparent">#00b25bf1</item>
-        <item name="colorAccent">#8e13e6</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.275" parent="AppTheme">
+        <item name="colorPrimary">#ad4ff1</item>
+        <item name="colorPrimaryTransparent">#00ad4ff1</item>
+        <item name="colorSecondary">#ad4ff1</item>
+        <item name="colorPrimaryDark">#8e29d6</item>
         <item name="table_row_dark_bg">#f4e7fd</item>
         <item name="table_row_light_bg">#fbf6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f4e7fd</item>
         <item name="table_row_light_bg">#fbf6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.280" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#be58f1</item>
-        <item name="colorPrimaryTransparent">#00be58f1</item>
-        <item name="colorAccent">#9e13e4</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.280" parent="AppTheme">
+        <item name="colorPrimary">#b746f0</item>
+        <item name="colorPrimaryTransparent">#00b746f0</item>
+        <item name="colorSecondary">#b746f0</item>
+        <item name="colorPrimaryDark">#9828d0</item>
         <item name="table_row_dark_bg">#f6e7fd</item>
         <item name="table_row_light_bg">#fbf6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f6e7fd</item>
         <item name="table_row_light_bg">#fbf6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.285" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ca56f1</item>
-        <item name="colorPrimaryTransparent">#00ca56f1</item>
-        <item name="colorAccent">#ae13e2</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.285" parent="AppTheme">
+        <item name="colorPrimary">#c23bef</item>
+        <item name="colorPrimaryTransparent">#00c23bef</item>
+        <item name="colorSecondary">#c23bef</item>
+        <item name="colorPrimaryDark">#a026c8</item>
         <item name="table_row_dark_bg">#f8e7fd</item>
         <item name="table_row_light_bg">#fcf6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f8e7fd</item>
         <item name="table_row_light_bg">#fcf6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.290" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#d654f1</item>
-        <item name="colorPrimaryTransparent">#00d654f1</item>
-        <item name="colorAccent">#be13e0</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.290" parent="AppTheme">
+        <item name="colorPrimary">#cd2aee</item>
+        <item name="colorPrimaryTransparent">#00cd2aee</item>
+        <item name="colorSecondary">#cd2aee</item>
+        <item name="colorPrimaryDark">#a224bc</item>
         <item name="table_row_dark_bg">#f9e7fd</item>
         <item name="table_row_light_bg">#fdf6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#f9e7fd</item>
         <item name="table_row_light_bg">#fdf6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.295" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#e351f0</item>
-        <item name="colorPrimaryTransparent">#00e351f0</item>
-        <item name="colorAccent">#cd13de</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.295" parent="AppTheme">
+        <item name="colorPrimary">#d713e9</item>
+        <item name="colorPrimaryTransparent">#00d713e9</item>
+        <item name="colorSecondary">#d713e9</item>
+        <item name="colorPrimaryDark">#9e20a9</item>
         <item name="table_row_dark_bg">#fbe7fd</item>
         <item name="table_row_dark_bg">#fbe7fd</item>
-        <item name="table_row_light_bg">#fdf6fe</item>
+        <item name="table_row_light_bg">#fef6fe</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.300" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#f04ef0</item>
-        <item name="colorPrimaryTransparent">#00f04ef0</item>
-        <item name="colorAccent">#db12db</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.300" parent="AppTheme">
+        <item name="colorPrimary">#dc12dc</item>
+        <item name="colorPrimaryTransparent">#00dc12dc</item>
+        <item name="colorSecondary">#dc12dc</item>
+        <item name="colorPrimaryDark">#a01ea0</item>
         <item name="table_row_dark_bg">#fde7fd</item>
         <item name="table_row_light_bg">#fef6fe</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7fd</item>
         <item name="table_row_light_bg">#fef6fe</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.305" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#f04ae2</item>
-        <item name="colorPrimaryTransparent">#00f04ae2</item>
-        <item name="colorAccent">#d912c8</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.305" parent="AppTheme">
+        <item name="colorPrimary">#e112cf</item>
+        <item name="colorPrimaryTransparent">#00e112cf</item>
+        <item name="colorSecondary">#e112cf</item>
+        <item name="colorPrimaryDark">#a31f98</item>
         <item name="table_row_dark_bg">#fde7fb</item>
         <item name="table_row_dark_bg">#fde7fb</item>
-        <item name="table_row_light_bg">#fef6fd</item>
+        <item name="table_row_light_bg">#fef6fe</item>
     </style>
 
     </style>
 
-    <style name="AppTheme.NoActionBar.310" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#f047d3</item>
-        <item name="colorPrimaryTransparent">#00f047d3</item>
-        <item name="colorAccent">#d612b5</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.310" parent="AppTheme">
+        <item name="colorPrimary">#e413c1</item>
+        <item name="colorPrimaryTransparent">#00e413c1</item>
+        <item name="colorSecondary">#e413c1</item>
+        <item name="colorPrimaryDark">#a6208f</item>
         <item name="table_row_dark_bg">#fde7f9</item>
         <item name="table_row_light_bg">#fef6fd</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f9</item>
         <item name="table_row_light_bg">#fef6fd</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.315" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ef43c4</item>
-        <item name="colorPrimaryTransparent">#00ef43c4</item>
-        <item name="colorAccent">#d312a3</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.315" parent="AppTheme">
+        <item name="colorPrimary">#e813b3</item>
+        <item name="colorPrimaryTransparent">#00e813b3</item>
+        <item name="colorSecondary">#e813b3</item>
+        <item name="colorPrimaryDark">#a92086</item>
         <item name="table_row_dark_bg">#fde7f8</item>
         <item name="table_row_light_bg">#fef6fc</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f8</item>
         <item name="table_row_light_bg">#fef6fc</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.320" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ef3fb4</item>
-        <item name="colorPrimaryTransparent">#00ef3fb4</item>
-        <item name="colorAccent">#d01190</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.320" parent="AppTheme">
+        <item name="colorPrimary">#eb13a3</item>
+        <item name="colorPrimaryTransparent">#00eb13a3</item>
+        <item name="colorSecondary">#eb13a3</item>
+        <item name="colorPrimaryDark">#ab217d</item>
         <item name="table_row_dark_bg">#fde7f6</item>
         <item name="table_row_light_bg">#fef6fb</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f6</item>
         <item name="table_row_light_bg">#fef6fb</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.325" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ee3aa3</item>
-        <item name="colorPrimaryTransparent">#00ee3aa3</item>
-        <item name="colorAccent">#cc117e</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.325" parent="AppTheme">
+        <item name="colorPrimary">#ec1a95</item>
+        <item name="colorPrimaryTransparent">#00ec1a95</item>
+        <item name="colorSecondary">#ec1a95</item>
+        <item name="colorPrimaryDark">#b02275</item>
         <item name="table_row_dark_bg">#fde7f4</item>
         <item name="table_row_light_bg">#fef6fb</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f4</item>
         <item name="table_row_light_bg">#fef6fb</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.330" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ee3692</item>
-        <item name="colorPrimaryTransparent">#00ee3692</item>
-        <item name="colorAccent">#c9116d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.330" parent="AppTheme">
+        <item name="colorPrimary">#ed2087</item>
+        <item name="colorPrimaryTransparent">#00ed2087</item>
+        <item name="colorSecondary">#ed2087</item>
+        <item name="colorPrimaryDark">#b5226c</item>
         <item name="table_row_dark_bg">#fde7f2</item>
         <item name="table_row_light_bg">#fef6fa</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f2</item>
         <item name="table_row_light_bg">#fef6fa</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.335" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ee3180</item>
-        <item name="colorPrimaryTransparent">#00ee3180</item>
-        <item name="colorAccent">#c5115c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.335" parent="AppTheme">
+        <item name="colorPrimary">#ed2679</item>
+        <item name="colorPrimaryTransparent">#00ed2679</item>
+        <item name="colorSecondary">#ed2679</item>
+        <item name="colorPrimaryDark">#b92362</item>
         <item name="table_row_dark_bg">#fde7f0</item>
         <item name="table_row_light_bg">#fef6f9</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7f0</item>
         <item name="table_row_light_bg">#fef6f9</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.340" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ed2c6d</item>
-        <item name="colorPrimaryTransparent">#00ed2c6d</item>
-        <item name="colorAccent">#c1104b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.340" parent="AppTheme">
+        <item name="colorPrimary">#ee2a6b</item>
+        <item name="colorPrimaryTransparent">#00ee2a6b</item>
+        <item name="colorSecondary">#ee2a6b</item>
+        <item name="colorPrimaryDark">#bc2456</item>
         <item name="table_row_dark_bg">#fde7ef</item>
         <item name="table_row_light_bg">#fef6f8</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7ef</item>
         <item name="table_row_light_bg">#fef6f8</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.345" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ed2759</item>
-        <item name="colorPrimaryTransparent">#00ed2759</item>
-        <item name="colorAccent">#bd103b</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.345" parent="AppTheme">
+        <item name="colorPrimary">#ee2d5d</item>
+        <item name="colorPrimaryTransparent">#00ee2d5d</item>
+        <item name="colorSecondary">#ee2d5d</item>
+        <item name="colorPrimaryDark">#be244b</item>
         <item name="table_row_dark_bg">#fde7ed</item>
         <item name="table_row_light_bg">#fef6f8</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7ed</item>
         <item name="table_row_light_bg">#fef6f8</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.350" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ec2244</item>
-        <item name="colorPrimaryTransparent">#00ec2244</item>
-        <item name="colorAccent">#b9102c</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.350" parent="AppTheme">
+        <item name="colorPrimary">#ee2f4f</item>
+        <item name="colorPrimaryTransparent">#00ee2f4f</item>
+        <item name="colorSecondary">#ee2f4f</item>
+        <item name="colorPrimaryDark">#c0253e</item>
         <item name="table_row_dark_bg">#fde7eb</item>
         <item name="table_row_light_bg">#fef6f7</item>
     </style>
 
         <item name="table_row_dark_bg">#fde7eb</item>
         <item name="table_row_light_bg">#fef6f7</item>
     </style>
 
-    <style name="AppTheme.NoActionBar.355" parent="AppTheme.NoActionBar">
-        <item name="colorPrimary">#ec1d2e</item>
-        <item name="colorPrimaryTransparent">#00ec1d2e</item>
-        <item name="colorAccent">#b50f1d</item>
-        <item name="drawer_background">#ffffffff</item>
+    <style name="AppTheme.355" parent="AppTheme">
+        <item name="colorPrimary">#ee3141</item>
+        <item name="colorPrimaryTransparent">#00ee3141</item>
+        <item name="colorSecondary">#ee3141</item>
+        <item name="colorPrimaryDark">#c12532</item>
         <item name="table_row_dark_bg">#fde7e9</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
         <item name="table_row_dark_bg">#fde7e9</item>
         <item name="table_row_light_bg">#fef6f6</item>
     </style>
 
     <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
 
 
     <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
 
-    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
+    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.DayNight" />
 
     <style name="nav_button">
         <item name="android:layout_width">match_parent</item>
 
     <style name="nav_button">
         <item name="android:layout_width">match_parent</item>
         <item name="android:paddingEnd">@dimen/activity_horizontal_margin</item>
     </style>
 
         <item name="android:paddingEnd">@dimen/activity_horizontal_margin</item>
     </style>
 
-    <style name="account_summary_account_name">
-        <item name="android:layout_width">wrap_content</item>
-        <item name="android:layout_height">wrap_content</item>
-        <item name="android:layout_weight">5</item>
-    </style>
-
     <style name="account_summary_amounts">
     <style name="account_summary_amounts">
-        <item name="android:layout_width">wrap_content</item>
-        <item name="android:layout_height">wrap_content</item>
-        <item name="android:layout_marginEnd">0dp</item>
-        <item name="android:layout_weight">1</item>
-        <item name="android:minWidth">60dp</item>
         <item name="android:textAlignment">viewEnd</item>
     </style>
 
         <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 name="transaction_list_comment">
+        <item name="android:textAppearance">@android:style/TextAppearance.Material.Small</item>
+        <item name="android:textColor">?commentColor</item>
     </style>
 
     <dimen name="thumb_row_height">48dp</dimen>
     </style>
 
     <dimen name="thumb_row_height">48dp</dimen>
+    <!-- px=rows×73+12-->
+    <!-- sp=px÷3.5 -->
+    <dimen name="default_account_row_height">64.2857sp</dimen>
 </resources>
 </resources>
diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml
deleted file mode 100644 (file)
index 93cbcae..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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>
index 223e12e8fbcb0f28f1559e621f1ebf197a23dbc2..c1da32275828ef31c0ccca046348a72b5a50e47f 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?><!--
 <?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
   ~ This file is part of MoLe.
   ~ 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>
   -->
 
 <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
 </network-security-config>
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_data_sync.xml b/app/src/main/res/xml/pref_data_sync.xml
deleted file mode 100644 (file)
index fcd1a38..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- NOTE: Hide buttons to simplify the UI. Users can touch outside the dialog to
-         dismiss it. -->
-    <!-- NOTE: ListPreference's summary should be set to its value by the activity code. -->
-    <ListPreference
-        android:defaultValue="180"
-        android:entries="@array/pref_sync_frequency_titles"
-        android:entryValues="@array/pref_sync_frequency_values"
-        android:key="sync_frequency"
-        android:negativeButtonText="@null"
-        android:positiveButtonText="@null"
-        android:title="@string/pref_title_sync_frequency" />
-
-    <!-- This preference simply launches an intent when selected. Use this UI sparingly, per
-         design guidelines. -->
-    <Preference android:title="@string/pref_title_system_sync_settings">
-        <intent android:action="android.settings.SYNC_SETTINGS" />
-    </Preference>
-
-</PreferenceScreen>
diff --git a/app/src/main/res/xml/pref_headers.xml b/app/src/main/res/xml/pref_headers.xml
deleted file mode 100644 (file)
index 1a49bad..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- These settings headers are only used on tablets. -->
-
-    <header
-        android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$InterfacePreferenceFragment"
-        android:icon="@drawable/ic_info_black_24dp"
-        android:title="@string/interface_pref_header_title" />
-<!--
-    <header
-        android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$NotificationPreferenceFragment"
-        android:icon="@drawable/ic_notifications_black_24dp"
-        android:title="@string/pref_header_notifications" />
-
-    <header
-        android:fragment="net.ktnx.mobileledger.ui.activity.SettingsActivity$DataSyncPreferenceFragment"
-        android:icon="@drawable/ic_sync_black_24dp"
-        android:title="@string/pref_header_data_sync" />
--->
-</preference-headers>
diff --git a/app/src/main/res/xml/pref_interface.xml b/app/src/main/res/xml/pref_interface.xml
deleted file mode 100644 (file)
index c1536be..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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/>.
-  -->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <SwitchPreference
-        android:id="@+id/pref_show_only_starred_accounts"
-        android:defaultValue="false"
-        android:key="pref_show_only_starred_accounts"
-        android:summaryOff="@string/pref_show_only_starred_off_summary"
-        android:summaryOn="@string/pref_show_only_starred_on_summary"
-        android:title="@string/menu_acc_summary_show_only_starred_title" />
-</PreferenceScreen>
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_notification.xml b/app/src/main/res/xml/pref_notification.xml
deleted file mode 100644 (file)
index 92273b6..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<!--
-  ~ 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/>.
-  -->
-
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <!-- A 'parent' preference, which enables/disables child preferences (below)
-         when checked/unchecked. -->
-    <SwitchPreference
-        android:defaultValue="true"
-        android:key="notifications_new_message"
-        android:title="@string/pref_title_new_message_notifications" />
-
-    <!-- Allows the user to choose a ringtone in the 'notification' category. -->
-    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
-    <!-- NOTE: RingtonePreference's summary should be set to its value by the activity code. -->
-    <RingtonePreference
-        android:defaultValue="content://settings/system/notification_sound"
-        android:dependency="notifications_new_message"
-        android:key="notifications_new_message_ringtone"
-        android:ringtoneType="notification"
-        android:title="@string/pref_title_ringtone" />
-
-    <!-- NOTE: This preference will be enabled only when the checkbox above is checked. -->
-    <SwitchPreference
-        android:defaultValue="true"
-        android:dependency="notifications_new_message"
-        android:key="notifications_new_message_vibrate"
-        android:title="@string/pref_title_vibrate" />
-
-</PreferenceScreen>
diff --git a/app/src/test/java/net/ktnx/mobileledger/async/LegacyParserTest.java b/app/src/test/java/net/ktnx/mobileledger/async/LegacyParserTest.java
new file mode 100644 (file)
index 0000000..985368a
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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 net.ktnx.mobileledger.model.LedgerTransactionAccount;
+
+import org.junit.Test;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertNull;
+
+public class LegacyParserTest {
+
+    private void expectParsedData(String input, String accountName, Float amount, String currency,
+                                  String comment) {
+        LedgerTransactionAccount lta = RetrieveTransactionsTask.parseTransactionAccountLine(input);
+
+        assertNotNull(lta);
+        assertEquals(accountName, lta.getAccountName());
+        assertEquals(amount, lta.getAmount());
+        assertEquals(currency, lta.getCurrency());
+        assertEquals(comment, lta.getComment());
+    }
+    private void expectNotParsed(String input) {
+        assertNull(RetrieveTransactionsTask.parseTransactionAccountLine(input));
+    }
+    @Test
+    public void parseTransactionAccountLine() {
+        expectParsedData(" acc:name  -34.56", "acc:name", -34.56f, null, null);
+        expectParsedData(" acc:name3  34.56", "acc:name3", 34.56f, null, null);
+        expectParsedData(" acc:name  +34.56", "acc:name", 34.56f, null, null);
+
+        expectParsedData(" acc:name  $-34.56", "acc:name", -34.56f, "$", null);
+        expectParsedData(" acc:name  $ -34.56", "acc:name", -34.56f, "$", null);
+        expectParsedData(" acc:name  -34.56$", "acc:name", -34.56f, "$", null);
+        expectParsedData(" acc:name  -34.56 $", "acc:name", -34.56f, "$", null);
+
+        expectParsedData(" acc:name  AU$-34.56", "acc:name", -34.56f, "AU$", null);
+        expectParsedData(" acc:name  AU$ -34.56", "acc:name", -34.56f, "AU$", null);
+        expectParsedData(" acc:name  -34.56AU$", "acc:name", -34.56f, "AU$", null);
+        expectParsedData(" acc:name  -34.56 AU$", "acc:name", -34.56f, "AU$", null);
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/net/ktnx/mobileledger/model/LedgerAccountTest.java b/app/src/test/java/net/ktnx/mobileledger/model/LedgerAccountTest.java
new file mode 100644 (file)
index 0000000..438e971
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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 static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class LedgerAccountTest {
+
+    @Test
+    public void extractParentName() {
+        assertNull(LedgerAccount.extractParentName("Top-level Account"));
+        assertEquals("top", LedgerAccount.extractParentName("top:second"));
+        assertEquals("top:second level", LedgerAccount.extractParentName("top:second level:leaf"));
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/net/ktnx/mobileledger/utils/SimpleDateTest.java b/app/src/test/java/net/ktnx/mobileledger/utils/SimpleDateTest.java
new file mode 100644 (file)
index 0000000..367ee3f
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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 org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+public class SimpleDateTest {
+
+    @After
+    public void tearDown() throws Exception {
+    }
+    @Test
+    public void compareTo() {
+        SimpleDate d1 = new SimpleDate(2020, 6, 1);
+        SimpleDate d2 = new SimpleDate(2019, 7, 6);
+
+        assertTrue(d1.compareTo(d2) > 0);
+        assertTrue(d2.compareTo(d1) < 0);
+        assertTrue(d1.compareTo(new SimpleDate(2020, 6, 2)) < 0);
+        assertTrue(d1.compareTo(new SimpleDate(2020, 5, 2)) > 0);
+        assertTrue(d1.compareTo(new SimpleDate(2019, 5, 2)) > 0);
+    }
+}
\ No newline at end of file
diff --git a/art/app-icon-transparent-bg.svg b/art/app-icon-transparent-bg.svg
new file mode 100644 (file)
index 0000000..be3b31d
--- /dev/null
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg4620"
+   version="1.1"
+   viewBox="0 0 108 108"
+   height="108mm"
+   width="108mm"
+   sodipodi:docname="app-icon-transparent-bg.svg"
+   inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
+  <sodipodi:namedview
+     inkscape:document-units="mm"
+     inkscape:snap-bbox="true"
+     inkscape:snap-to-guides="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:guide-bbox="true"
+     showguides="true"
+     inkscape:document-rotation="0"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1022"
+     id="namedview849"
+     showgrid="false"
+     inkscape:zoom="1"
+     inkscape:cx="204.09449"
+     inkscape:cy="190.26354"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer2"
+     inkscape:pagecheckerboard="true">
+    <sodipodi:guide
+       position="18.000001,75.191669"
+       orientation="-1,0"
+       id="guide863"
+       inkscape:label=""
+       inkscape:locked="true"
+       inkscape:color="rgb(239,41,41)" />
+    <sodipodi:guide
+       position="4.3656251,90.000003"
+       orientation="0,1"
+       id="guide865"
+       inkscape:label=""
+       inkscape:locked="true"
+       inkscape:color="rgb(239,41,41)" />
+    <sodipodi:guide
+       position="90.000003,96.490628"
+       orientation="-1,0"
+       id="guide867"
+       inkscape:label=""
+       inkscape:locked="true"
+       inkscape:color="rgb(239,41,41)" />
+    <sodipodi:guide
+       position="11.377084,18.000128"
+       orientation="0,1"
+       id="guide869"
+       inkscape:label=""
+       inkscape:locked="true"
+       inkscape:color="rgb(239,41,41)" />
+    <sodipodi:guide
+       inkscape:color="rgb(115,210,22)"
+       inkscape:locked="true"
+       inkscape:label=""
+       id="guide1056"
+       orientation="-1,0"
+       position="54.000002,14" />
+    <sodipodi:guide
+       inkscape:color="rgb(115,210,22)"
+       inkscape:locked="true"
+       inkscape:label=""
+       id="guide1058"
+       orientation="0,1"
+       position="-10.179975,54.000002" />
+  </sodipodi:namedview>
+  <title
+     id="title921">MoLe app icon</title>
+  <defs
+     id="defs4614" />
+  <metadata
+     id="metadata4617">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title>MoLe app icon</dc:title>
+        <cc:license
+           rdf:resource="https://spdx.org/licenses/GPL-3.0-or-later.html" />
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Damyan Ivanov &lt;dam+mole@ktnx.net&gt;</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:rights>
+          <cc:Agent>
+            <dc:title>Copyright © 2019 Damyan Ivanov &lt;dam+mole@ktnx.net&gt;. All rights reserved.</dc:title>
+          </cc:Agent>
+        </dc:rights>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="layer2"
+     inkscape:label="Layer 2"
+     style="display:inline">
+    <g
+       transform="translate(-2e-6,0.00461348)"
+       id="g1076">
+      <path
+         sodipodi:nodetypes="ccccccc"
+         id="path966"
+         d="m 42.059608,32.591491 c -1.330928,1.331242 -1.329987,3.48957 0.0021,4.81965 1.331008,1.329564 3.487755,1.328622 4.817602,-0.0021 3.961687,-3.962245 10.280239,-3.962245 14.241925,0 1.329848,1.330724 3.486595,1.331666 4.817603,0.0021 1.332089,-1.33008 1.333031,-3.488408 0.0021,-4.81965 -7.522511,-6.703268 -17.464451,-6.432765 -23.881335,0 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.1924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1" />
+      <path
+         sodipodi:nodetypes="ccccccc"
+         id="path970"
+         d="m 32.530868,23.615557 c -1.330143,1.330427 -1.330143,3.487175 0,4.817602 1.330427,1.330143 3.487176,1.330143 4.817603,0 9.225233,-9.226534 24.078267,-9.226534 33.3035,0 1.330428,1.330143 3.487176,1.330143 4.817603,0 1.330143,-1.330427 1.330143,-3.487175 0,-4.817602 -12.951936,-12.351289 -31.974796,-11.302262 -42.938706,0 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7.1924;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1" />
+      <path
+         d="m 17.520218,41.911281 c -3.229012,0.666018 -3.355024,3.178313 -3.355024,3.826683 V 89.40568 c 0,2.088019 1.745629,3.836975 3.834806,3.836975 13.048184,-2.198993 19.955715,-2.120531 36.000272,0.007 C 68.789639,91.20757 79.58749,90.993831 90,93.242655 c 2.089179,0 3.83481,-1.748956 3.83481,-3.836975 V 45.737964 c 0,-2.094287 -1.813541,-3.538325 -3.635998,-3.826683 -9.490786,-1.501681 -13.337816,-2.215527 -27.678433,-0.944752 0.484137,1.474537 0.720056,2.785206 0.442041,4.184214 10.721463,-1.146399 19.131227,-0.668351 26.468776,0.914958 l -0.04496,42.916353 C 77.901708,86.915793 70.894084,86.954988 56.213045,88.794626 V 52.769435 c -1.421517,0.34798 -3.104446,0.33235 -4.42609,0 V 88.794626 C 40.094451,86.924228 28.873452,86.925073 18.591283,88.982054 V 46.065701 c 11.034938,-1.396761 15.891637,-2.04558 26.444134,-0.914958 -0.180306,-1.44057 0.02152,-2.978335 0.450706,-4.184214 -9.086409,-1.124136 -17.013783,-0.645583 -27.965905,0.944752 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.42609;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
+         id="path954"
+         sodipodi:nodetypes="csscccsssccccccccccccc" />
+      <path
+         d="M 32.772594,55.424384 V 65.96076 H 22.238386 v 5.13655 h 10.534208 v 10.534207 h 5.137091 V 71.09731 H 48.445517 V 65.96076 H 37.909685 V 55.424384 Z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:5.42196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
+         id="path978" />
+      <path
+         id="path974"
+         d="M 59.352204,65.960507 V 71.09751 H 85.55931 v -5.137003 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:5.42196;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1" />
+      <path
+         id="path5169"
+         d="m 54.000221,40.573228 c 1.763789,0 3.406922,1.645178 3.406924,3.406914 4e-6,1.761747 -1.643133,3.403181 -3.406924,3.403181 -1.76379,0 -3.406928,-1.641434 -3.406924,-3.403181 2e-6,-1.761736 1.643135,-3.406914 3.406924,-3.406914 z"
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:11.5078;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+    </g>
+  </g>
+</svg>
index fb72d77a1eca49d1e2be61df91dd0b8ddb3bafa2..a66927bb5cdbee4eaca9da8f86a1c1192e301f24 100644 (file)
@@ -1,6 +1,4 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
 <svg
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:cc="http://creativecommons.org/ns#"
 <svg
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:cc="http://creativecommons.org/ns#"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="100mm"
-   height="100mm"
-   viewBox="0 0 100 100"
-   version="1.1"
+   sodipodi:docname="app-icon.svg"
+   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
    id="svg4620"
    id="svg4620"
-   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
-   sodipodi:docname="app-icon.svg">
+   version="1.1"
+   viewBox="0 0 48.000001 48.000001"
+   height="48"
+   width="48">
   <title
      id="title921">MoLe app icon</title>
   <defs
      id="defs4614" />
   <sodipodi:namedview
   <title
      id="title921">MoLe app icon</title>
   <defs
      id="defs4614" />
   <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="2.0032945"
-     inkscape:cx="170.76502"
-     inkscape:cy="219.44174"
-     inkscape:document-units="mm"
-     inkscape:current-layer="layer3"
-     showgrid="false"
-     inkscape:snap-page="true"
-     showguides="false"
-     inkscape:guide-bbox="true"
-     inkscape:snap-object-midpoints="true"
-     showborder="false"
-     inkscape:window-width="1920"
-     inkscape:window-height="1045"
-     inkscape:window-x="0"
-     inkscape:window-y="35"
-     inkscape:window-maximized="1"
-     inkscape:pagecheckerboard="true"
-     inkscape:snap-bbox="true"
-     inkscape:bbox-nodes="true"
+     units="px"
+     inkscape:document-rotation="0"
+     inkscape:snap-to-guides="true"
      inkscape:bbox-paths="true"
      inkscape:bbox-paths="true"
-     inkscape:snap-to-guides="true">
-    <sodipodi:guide
-       position="50,138.6761"
-       orientation="1,0"
-       id="guide5186"
-       inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="70.959076,4"
-       orientation="0,1"
-       id="guide5236"
-       inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="6,-7.7831425"
-       orientation="1,0"
-       id="guide5240"
-       inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="56.615925,63.290398"
-       orientation="0,1"
-       id="guide5242"
-       inkscape:locked="false" />
-    <sodipodi:guide
-       position="110.53864,6"
-       orientation="0,1"
-       id="guide5295"
-       inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="50.407291,60.491062"
-       orientation="0,1"
-       id="guide5297"
-       inkscape:locked="false" />
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox="true"
+     inkscape:pagecheckerboard="true"
+     inkscape:window-maximized="1"
+     inkscape:window-y="0"
+     inkscape:window-x="0"
+     inkscape:window-height="1022"
+     inkscape:window-width="1920"
+     showborder="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:guide-bbox="true"
+     showguides="true"
+     inkscape:snap-page="true"
+     showgrid="false"
+     inkscape:current-layer="layer3"
+     inkscape:document-units="px"
+     inkscape:cy="29.274522"
+     inkscape:cx="31.564456"
+     inkscape:zoom="6.7245436"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     borderopacity="1.0"
+     bordercolor="#666666"
+     pagecolor="#ffffff"
+     id="base">
     <sodipodi:guide
     <sodipodi:guide
-       position="93.999999,-2.1077283"
-       orientation="1,0"
-       id="guide5299"
-       inkscape:locked="false"
+       inkscape:color="rgb(0,0,255)"
        inkscape:label=""
        inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="71.999999,17.719069"
-       orientation="1,0"
-       id="guide5301"
        inkscape:locked="false"
        inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
+       id="guide1407"
+       orientation="-1,0"
+       position="24,-97.820938" />
     <sodipodi:guide
     <sodipodi:guide
-       position="28,-19.706254"
-       orientation="1,0"
-       id="guide5309"
-       inkscape:locked="false"
-       inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
+       id="guide29"
+       orientation="-1,0"
+       position="24,-97.820938" />
     <sodipodi:guide
     <sodipodi:guide
-       position="6,26.118209"
-       orientation="1,0"
-       id="guide1407"
+       inkscape:color="rgb(0,0,255)"
        inkscape:locked="false"
        inkscape:label=""
        inkscape:locked="false"
        inkscape:label=""
-       inkscape:color="rgb(0,0,255)" />
-    <sodipodi:guide
-       position="50.000001,59.08557"
+       id="guide31"
        orientation="0,1"
        orientation="0,1"
-       id="guide835"
-       inkscape:locked="false" />
+       position="-10.658475,24" />
   </sodipodi:namedview>
   <metadata
      id="metadata4617">
   </sodipodi:namedview>
   <metadata
      id="metadata4617">
     </rdf:RDF>
   </metadata>
   <g
     </rdf:RDF>
   </metadata>
   <g
-     inkscape:groupmode="layer"
+     inkscape:label="фон"
      id="layer3"
      id="layer3"
-     inkscape:label="фон">
+     inkscape:groupmode="layer">
     <rect
     <rect
-       style="opacity:1;fill:#935ff2;fill-opacity:1;stroke:none;stroke-width:6.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1;paint-order:markers stroke fill"
-       id="rect5267"
-       width="100"
-       height="100"
-       x="0"
+       ry="3.8399999"
        y="0"
        y="0"
-       ry="8" />
+       x="0"
+       height="48"
+       width="48"
+       id="rect5267"
+       style="opacity:1;fill:#935ff2;fill-opacity:1;stroke:none;stroke-width:3.12;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;paint-order:markers stroke fill" />
   </g>
   <g
   </g>
   <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1"
+     style="opacity:1"
      transform="translate(0,-197)"
      transform="translate(0,-197)"
-     style="opacity:1">
-    <path
-       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1"
-       id="path5171"
-       sodipodi:type="arc"
-       sodipodi:cx="50"
-       sodipodi:cy="238.4247"
-       sodipodi:rx="12.857347"
-       sodipodi:ry="12.859161"
-       sodipodi:start="3.9269908"
-       sodipodi:end="5.4977871"
-       d="m 40.908483,229.3319 a 12.857347,12.859161 0 0 1 18.183034,0"
-       sodipodi:open="true" />
-    <path
-       sodipodi:open="true"
-       d="m 31.816966,220.76826 a 25.714693,25.718323 0 0 1 36.366067,0"
-       sodipodi:end="5.4977871"
-       sodipodi:start="3.9269908"
-       sodipodi:ry="25.718323"
-       sodipodi:rx="25.714693"
-       sodipodi:cy="238.95386"
-       sodipodi:cx="50"
-       sodipodi:type="arc"
-       id="path5179"
-       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:6.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1" />
-    <path
-       style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1"
-       d="m 15.657508,237.91443 c 0,0 10.479872,-1.46121 15.754569,-1.49924 6.217409,-0.0448 18.593412,1.49924 18.593412,1.49924 0,0 12.821397,-1.56825 19.26017,-1.49924 5.049678,0.0542 15.076834,1.49924 15.076834,1.49924 0.91445,0.087 1.658077,0.74015 1.658077,1.65956 v 41.65656 c 0,0.91939 -0.739503,1.65956 -1.658077,1.65956 0,0 -10.02555,-1.49483 -15.076834,-1.54758 -6.440667,-0.0673 -19.26017,1.5713 -19.26017,1.5713 0,0 -12.374045,-1.61362 -18.593412,-1.5713 -5.276236,0.0359 -15.754569,1.54758 -15.754569,1.54758 -0.918576,0 -1.658078,-0.74017 -1.658078,-1.65956 v -41.65656 c 0,-0.91941 0.739502,-1.65956 1.658078,-1.65956 z"
-       id="rect5165"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="sacassssacassss" />
-    <path
-       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.00000048;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       d="M 50.000001,281.74975 V 237.91443"
-       id="path5167"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <path
-       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.9000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
-       d="m 19.700791,261.31466 h 25"
-       id="path5216"
-       inkscape:connector-curvature="0"
-       sodipodi:nodetypes="cc" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path5218"
-       d="m 32.200791,273.81466 v -25"
-       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.9000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-    <path
-       sodipodi:nodetypes="cc"
-       inkscape:connector-curvature="0"
-       id="path5220"
-       d="m 55.105263,261.31466 h 25"
-       style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:4.9000001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
-    <path
-       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#935ff2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.39999962;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
-       d="m 50,229.26562 c -4.704272,0 -8.628915,3.92712 -8.628906,8.63086 4e-6,4.70374 3.924643,8.62891 8.628906,8.62891 4.704263,0 8.628902,-3.92517 8.628906,-8.62891 9e-6,-4.70374 -3.924634,-8.63086 -8.628906,-8.63086 z"
-       id="path844" />
-    <path
-       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.39999962;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.08858268;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
-       d="m 50,234.6473 c 1.682548,0 3.249998,1.5694 3.25,3.24999 4e-6,1.6806 -1.56745,3.24643 -3.25,3.24643 -1.68255,0 -3.250004,-1.56583 -3.25,-3.24643 2e-6,-1.68059 1.567452,-3.24999 3.25,-3.24999 z"
-       id="path5169"
-       inkscape:connector-curvature="0" />
+     id="layer1"
+     inkscape:groupmode="layer"
+     inkscape:label="Layer 1">
+    <g
+       transform="translate(0,-2.9995236)"
+       id="g892">
+      <path
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1"
+         id="path5171"
+         sodipodi:type="arc"
+         sodipodi:cx="24"
+         sodipodi:cy="216.88385"
+         sodipodi:rx="6.1715264"
+         sodipodi:ry="6.1723976"
+         sodipodi:start="3.9269908"
+         sodipodi:end="5.4977871"
+         d="m 19.636072,212.51931 a 6.1715264,6.1723976 0 0 1 8.727856,0"
+         sodipodi:open="true"
+         sodipodi:arc-type="arc" />
+      <path
+         sodipodi:open="true"
+         d="m 15.272143,208.40876 a 12.343053,12.344795 0 0 1 17.455713,0"
+         sodipodi:end="5.4977871"
+         sodipodi:start="3.9269908"
+         sodipodi:ry="12.344795"
+         sodipodi:rx="12.343053"
+         sodipodi:cy="217.13785"
+         sodipodi:cx="24"
+         sodipodi:type="arc"
+         id="path5179"
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1"
+         sodipodi:arc-type="arc" />
+      <path
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:2.03838;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1"
+         d="m 6.8473329,216.65689 c 0,0 5.2342651,-0.70018 7.8687611,-0.71841 3.105341,-0.0215 9.286648,0.71841 9.286648,0.71841 0,0 6.403762,-0.75147 9.619664,-0.71841 2.522109,0.026 7.530262,0.71841 7.530262,0.71841 0.456729,0.0418 0.82814,0.35465 0.82814,0.79521 v 19.96076 c 0,0.44055 -0.369351,0.79522 -0.82814,0.79522 0,0 -5.007349,-0.71629 -7.530262,-0.74156 -3.216849,-0.0323 -9.619664,0.75293 -9.619664,0.75293 0,0 -6.180328,-0.7732 -9.286648,-0.75293 -2.635263,0.0172 -7.8687611,0.74156 -7.8687611,0.74156 -0.458791,0 -0.828142,-0.35467 -0.828142,-0.79522 V 217.4521 c 0,-0.44056 0.369351,-0.79521 0.828142,-0.79521 z"
+         id="rect5165"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="sacassssacassss" />
+      <path
+         style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 24,237.67988 V 216.63893"
+         id="path5167"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc" />
+      <path
+         style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         d="M 9,228 H 21"
+         id="path5216"
+         inkscape:connector-curvature="0"
+         sodipodi:nodetypes="cc" />
+      <path
+         sodipodi:nodetypes="cc"
+         inkscape:connector-curvature="0"
+         id="path5218"
+         d="M 15,234 V 222"
+         style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="cc"
+         inkscape:connector-curvature="0"
+         id="path5220"
+         d="M 27,228 H 39"
+         style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#935ff2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.992;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+         d="m 24,212.4875 c -2.258051,0 -4.141879,1.88502 -4.141875,4.14281 2e-6,2.2578 1.883829,4.14188 4.141875,4.14188 2.258046,0 4.141873,-1.88408 4.141875,-4.14188 4e-6,-2.25779 -1.883824,-4.14281 -4.141875,-4.14281 z"
+         id="path844" />
+      <path
+         style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0.0885827;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+         d="m 24,215.0707 c 0.807623,0 1.559999,0.75332 1.56,1.56 2e-6,0.80669 -0.752376,1.55829 -1.56,1.55829 -0.807624,0 -1.560002,-0.7516 -1.56,-1.55829 10e-7,-0.80668 0.752377,-1.56 1.56,-1.56 z"
+         id="path5169"
+         inkscape:connector-curvature="0" />
+    </g>
   </g>
 </svg>
   </g>
 </svg>
diff --git a/art/thick-plus-icon.svg b/art/thick-plus-icon.svg
new file mode 100644 (file)
index 0000000..a10e929
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
+    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+    xmlns:xlink="http://www.w3.org/1999/xlink" height="100mm" id="svg8" version="1.1"
+    viewBox="0 0 100 100" width="100mm" xmlns="http://www.w3.org/2000/svg"
+    inkscape:version="1.0 (4035a4fb49, 2020-05-01)" sodipodi:docname="thick-plus-icon.svg">
+  <defs id="defs2" />
+  <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" pagecolor="#ffffff"
+      showgrid="false" inkscape:current-layer="layer1" inkscape:cx="188.97638"
+      inkscape:cy="188.97639" inkscape:document-rotation="0" inkscape:document-units="mm"
+      inkscape:pagecheckerboard="true" inkscape:pageopacity="0.0" inkscape:pageshadow="2"
+      inkscape:window-height="1022" inkscape:window-maximized="1" inkscape:window-width="1920"
+      inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="2" />
+  <metadata id="metadata5">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g id="layer1" inkscape:groupmode="layer" inkscape:label="Слой 1">
+    <circle
+        style="fill:#ededed;fill-opacity:1;stroke:none;stroke-width:0.370416;stroke-linecap:round;stroke-linejoin:bevel;paint-order:markers stroke fill"
+        cx="50" cy="50" id="path833" r="35" />
+    <path
+        style="fill:none;fill-rule:evenodd;stroke:#935ff2;stroke-width:9;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+        d="M 50.18969,35 V 65" id="path835" />
+    <use style="stroke-width:1;stroke-miterlimit:4;stroke-dasharray:none" height="100%" id="use919"
+        transform="rotate(-90,50.094845,50.094845)" width="100%" x="0" y="0"
+        xlink:href="#path835" />
+  </g>
+</svg>
index e3b999bd45d9674697322285d44bd29bc7083054..e9d71576e71beaaba39d96d67f402a45e9f63e69 100644 (file)
@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright © 2019 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
  * This file is part of MoLe.
  * MoLe is free software: you can distribute it and/or modify it
  * under the term of the GNU General Public License as published by
@@ -21,10 +21,10 @@ buildscript {
     
     repositories {
         google()
     
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
     dependencies {
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:3.5.3'
+        classpath 'com.android.tools.build:gradle:8.0.2'
         
 
         // NOTE: Do not place your application dependencies here; they belong
         
 
         // NOTE: Do not place your application dependencies here; they belong
@@ -35,7 +35,7 @@ buildscript {
 allprojects {
     repositories {
         google()
 allprojects {
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
 }
 
     }
 }
 
index 048e816edeb7bd5d8292f38228ae641cf4edf448..db96c7725ba87fd8ea2d7a50722e54fe5ebacccf 100644 (file)
@@ -1,5 +1,5 @@
 #
 #
-# 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
 # This file is part of MoLe.
 # 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/>.
 #
 
 # 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
 # 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.
 # 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
 # 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.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.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
index 4251e445b4cf7810b550e4bf0026c57e28b0923e..855f89cc8328ec83f5f1cf99a034a4c0aa8cd550 100644 (file)
@@ -1,6 +1,21 @@
-#Thu Sep 12 18:32:06 EEST 2019
+#
+# 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
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
index f9553162f122c71b34635112e717c3e733b5b212..e95643d6a2ca62258464e83c72f5156dc941c609 100644 (file)
@@ -1,84 +1,84 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@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
index 710abda529aa592a1e8e7154e9dc54a158c3c7ef..d1b0beecc738edb793e91fdba5a74421c38faefe 100644 (file)
@@ -7,10 +7,10 @@
 * ПОДОБРЕНИЯ
   - поясняване, че докладите за грешка се изпращат по email и потребителят има възможност да ги прегледа преди изпращане
   - добавена възможност за преглед на паролата в настройките на профилите
 * ПОДОБРЕНИЯ
   - поясняване, че докладите за грешка се изпращат по email и потребителят има възможност да ги прегледа преди изпращане
   - добавена възможност за преглед на паролата в настройките на профилите
-  - преработен екран за въвеждане на трансакция
+  - преработен екран за въвеждане на транзакция
 * ПОПРАВКИ
 * ПОПРАВКИ
-  - повторно активиране на директните икони за въвеждане на трансакция на Android 7.1 (Nougat)
-  - отстранен възможен срив при връщане към екрана за въвеждане на трансакция от друго приложение
+  - повторно активиране на директните икони за въвеждане на транзакция на Android 7.1 (Nougat)
+  - отстранен възможен срив при връщане към екрана за въвеждане на транзакция от друго приложение
   - отсранен проблем при настройване на цветовете на темата
   - отсранен проблем при настройване на цветовете на темата
-  - завъртането на екрана вече не изчиства екрана за въвеждане на трансакция
+  - завъртането на екрана вече не изчиства екрана за въвеждане на транзакция
   - поправка в JSON API за hledger-web 1.15.2
   - поправка в JSON API за hledger-web 1.15.2
diff --git a/metadata/bg-BG/changelogs/31.txt b/metadata/bg-BG/changelogs/31.txt
new file mode 100644 (file)
index 0000000..11262ad
--- /dev/null
@@ -0,0 +1,16 @@
+Всички промени: <https://mole.ktnx.net/#index6h1>
+
+* НОВО
+    + коментари към сметките и избор на валута/актив при въвеждане на транзакция
+    + контролирано въвеждане на бъдещи дати
+    + работа с версии 1.14 и 1.15+ на JSON API
+* ПОДОБРЕНИЯ
+    + по-тъмни нюанси в жълто-зеления спектър
+    + уникален цвят при създаване на профил
+    + подобрена работа на настройките на профилите
+    + по-гладка работа на интерфейса за добавяне на транзакция
+* FIXES
+    + поправено превъртане на настройките на профилите
+    + премахване на всички данни свързани с премахвания профил
+    + отстранен „залепнал“ индикатор за фонов процес при обновяване на транзакциите
+    + отстранен проблем при добавяне на бързи връзки
diff --git a/metadata/bg-BG/changelogs/32.txt b/metadata/bg-BG/changelogs/32.txt
new file mode 100644 (file)
index 0000000..555e071
--- /dev/null
@@ -0,0 +1,7 @@
+* НОВО
+    + въведане на бележка за цялата транзакция
+    + скриване на полетата за бележки за профила
+* ПОПРАВКИ:
+    + отстранен срив при разчитане на флагове с hledger-web преди версия 1.14
+    + визуални поправки
+    + отстранен проблем при въвеждане на числа с някои кодели на Samsung
diff --git a/metadata/bg-BG/changelogs/33.txt b/metadata/bg-BG/changelogs/33.txt
new file mode 100644 (file)
index 0000000..1ce5545
--- /dev/null
@@ -0,0 +1 @@
+Допълнителна, универсална поправка на въвеждането на числа
diff --git a/metadata/bg-BG/changelogs/34.txt b/metadata/bg-BG/changelogs/34.txt
new file mode 100644 (file)
index 0000000..3a1b8d7
--- /dev/null
@@ -0,0 +1,10 @@
+* НОВО
+    + показване на бележките към транзакциите
+    + отиване до избрана дата в списъка с транзакции
+* ПОДОБРЕНО
+    + визуални подобрения; използване на препоръки от material design
+    + използване на системната големина на шрифтовете
+* ПОПРАВКИ
+    + срив при работа с hledger-web преди 1.14 и суми от вида „$123.45“
+    + срив при смяна на темата на профила
+    + други дребни поправки
diff --git a/metadata/bg-BG/changelogs/35.txt b/metadata/bg-BG/changelogs/35.txt
new file mode 100644 (file)
index 0000000..19685a2
--- /dev/null
@@ -0,0 +1,4 @@
+* ПОДОБРЕНИЯ
+    + корекции на шрифтове и цветове, особено при тъмна системна тема
+* FIXES
+    + възстановаяване на изображението в списъка с приложения на f-droid
diff --git a/metadata/bg-BG/changelogs/36.txt b/metadata/bg-BG/changelogs/36.txt
new file mode 100644 (file)
index 0000000..276da5a
--- /dev/null
@@ -0,0 +1,12 @@
+
+* НОВО
+    + стартиращ екран
+    + брои сметки и транзакции
+* ПОДОБРЕНО
+    + поправки в цветовата схема, подобрен контраст
+    + по-бързи реакции, още задачи във фонови процеси
+    + по-бързо съхраняване на изтеглените данни
+* ПОПРАВЕНО
+    + напредък при изтегляне
+    + излишни многократни изтегляния
+    + попълване на списъка с валути
diff --git a/metadata/bg-BG/changelogs/37.txt b/metadata/bg-BG/changelogs/37.txt
new file mode 100644 (file)
index 0000000..62d5605
--- /dev/null
@@ -0,0 +1,10 @@
+
+* НОВО
+    + поддръжка на последната версия на протокола за комуникация (hledger-web 1.19.1)
+    + откриване на версията на сървъра
+    + поддръжка на повече от една версия на протокола за комуникация
+* ПОДОБРЕНО
+    + настройките на базата данни се извършват във фонов режим, докато се показва началния екран
+* ПОПРАВЕНО
+    + използване на валутата по подразбиране при въвеждане на ново движение
+    + отстранени няколко срива
diff --git a/metadata/bg-BG/changelogs/38.txt b/metadata/bg-BG/changelogs/38.txt
new file mode 100644 (file)
index 0000000..41a6cfb
--- /dev/null
@@ -0,0 +1,7 @@
+
+* NEW
+    + макети за движения, прилагани чрез сканиране на QR код
+* IMPROVEMENTS
+    + по-голям бутон за валута при новите движения
+    + унифицирано поведение на хвърчащия бутон за добавяне/съхраняване
+    + частична миграция към напълно асинхронен слой за връзка с базата данни
diff --git a/metadata/bg-BG/changelogs/39.txt b/metadata/bg-BG/changelogs/39.txt
new file mode 100644 (file)
index 0000000..c978964
--- /dev/null
@@ -0,0 +1,3 @@
+
+* ПОПРАВКИ
+    + отстранен проблем при миграцията на данните при профили без проверена версия на сървъра
diff --git a/metadata/bg-BG/changelogs/40.txt b/metadata/bg-BG/changelogs/40.txt
new file mode 100644 (file)
index 0000000..db85b5d
--- /dev/null
@@ -0,0 +1,11 @@
+
+* НОВО
+    + новите движения са видими в списъка с движения без да е нужно презареждане
+* ПОДОБРЕНО
+    + завършена миграция към напълно асинхронен слой за връзка с базата данни
+    + подобрено поведение при превключване от списъка със сметки към списъка с движения за пръв път
+* ПОПРАВЕНО
+    + визуални проблеми в редактора на шаблони
+    + поправена обработка на грешки при изпробване на различни версии на протокола за връзка с hledger-web
+    + без изчистване на датата при зареждане на старо движение
+    + няколко по-малки поправки
diff --git a/metadata/bg-BG/changelogs/41.txt b/metadata/bg-BG/changelogs/41.txt
new file mode 100644 (file)
index 0000000..5ddb0cc
--- /dev/null
@@ -0,0 +1,12 @@
+
+* НОВОСТИ
+    + поддръжка на валута в шаблоните
+    + баланс с натрупване при филтриране на движенията по сметка
+    + текущ баланс при избор на сметка (ново движение)
+* ПОДОБРЕНИЯ
+    + по-отчетлив фон на изскачащия прозорец при избор на сметка при тъмна тема
+    + подобрено оформление на балансите про много дълги имена на сметки
+* ПОПРАВКИ
+    + зачитане на валутата по подразбиране при въвеждане на ново движение
+    + отразяване на промените в текущо активния профил
+    + поправено разнасяне на новите движения по родителските сметки
diff --git a/metadata/bg-BG/changelogs/42.txt b/metadata/bg-BG/changelogs/42.txt
new file mode 100644 (file)
index 0000000..448903a
--- /dev/null
@@ -0,0 +1,4 @@
+* ПОПРАВКИ
+    + отстранен проблем при ново движение и въвеждане на невалидна сума
+    + поправено зареждане на предишно движение по описание (отново)
+    + отстранен срив при разпознаване на версия на hledger, състояща се само от два компонента
diff --git a/metadata/bg-BG/changelogs/43.txt b/metadata/bg-BG/changelogs/43.txt
new file mode 100644 (file)
index 0000000..ffc037b
--- /dev/null
@@ -0,0 +1,2 @@
+* ПОПРАВКИ
+    + поправено намиране на стари движения при въвеждане на описание на ново движение на някои варианти/версии на Андроид (повредено във версия 0.18.0)
diff --git a/metadata/bg-BG/changelogs/44.txt b/metadata/bg-BG/changelogs/44.txt
new file mode 100644 (file)
index 0000000..59b4578
--- /dev/null
@@ -0,0 +1,4 @@
+* НОВО
+    + съхраняване на резервни копия на всички профили и шаблони; възстановяване от резервно копие
+* ПОПРАВКИ
+    + няколко срива свързани със стартиране на екрана за ново двишение от пряк път
diff --git a/metadata/bg-BG/changelogs/45.txt b/metadata/bg-BG/changelogs/45.txt
new file mode 100644 (file)
index 0000000..4d4824f
--- /dev/null
@@ -0,0 +1,6 @@
+* ПОПРАВКИ
+    + Ново движение: преместване на фокуса в полето за сума след избиране на сметка
+    + Ново движение: отстранен срив при връщане в приложението когато няма фокусиран елемент
+    + отстранен срив при обновяване на БД до версия 0.20.0
+    + поправено възстановяване на настройките при налични празни стойности
+    + без използване на AsyncTask -- вече не се препоръчва
diff --git a/metadata/bg-BG/changelogs/46.txt b/metadata/bg-BG/changelogs/46.txt
new file mode 100644 (file)
index 0000000..32845dd
--- /dev/null
@@ -0,0 +1,4 @@
+* НОВО
+    + архивно копие в облака
+* ПОПРАВКИ
+    + два проблема в работата на базата данни, единият причиняващ срив при стартиране
diff --git a/metadata/bg-BG/changelogs/47.txt b/metadata/bg-BG/changelogs/47.txt
new file mode 100644 (file)
index 0000000..ecb69e4
--- /dev/null
@@ -0,0 +1,2 @@
+* ПОПРАВКИ
+    + отстранен още един проблем при обновяване на БД от версия 0.16.0
diff --git a/metadata/bg-BG/changelogs/48.txt b/metadata/bg-BG/changelogs/48.txt
new file mode 100644 (file)
index 0000000..b92f6e0
--- /dev/null
@@ -0,0 +1,4 @@
+* ИЗВЕСТНИ ПРОБЛЕМИ
+    + Несъвместимост с hledger-web 1.23+
+* ПОПРАВКИ
+    + поправено дописване на описанието при въвеждане на ново движение
diff --git a/metadata/bg-BG/changelogs/49.txt b/metadata/bg-BG/changelogs/49.txt
new file mode 100644 (file)
index 0000000..fbb1844
--- /dev/null
@@ -0,0 +1,4 @@
+* НОВО
+    + Добавена поддръжка на hledger-web версия 1.23
+* ПОПРАВКИ
+    + Включване на поддържащ файл за работата на БД, пропъснат във версия 0.20.4
diff --git a/metadata/bg-BG/changelogs/50.txt b/metadata/bg-BG/changelogs/50.txt
new file mode 100644 (file)
index 0000000..bdf0e80
--- /dev/null
@@ -0,0 +1,4 @@
+* ПОПРАВКИ
+    + поддръжане на hledger-web 1.23 и при добавяне на нови движения
+    + поправена натрупана сума при добавяне на движение в миналото
+    + поправен срив при изпращане на ново движение без данни за суми
diff --git a/metadata/bg-BG/changelogs/51.txt b/metadata/bg-BG/changelogs/51.txt
new file mode 100644 (file)
index 0000000..0af9229
--- /dev/null
@@ -0,0 +1,6 @@
+* ПОПРАВКИ
+    + отстранен срив при балансиране на трансакция с повече от една валута
+    + отстранен срив при дублиране на макет
+    + отстранен срив при зареждане на настройки от резервно копие
+* ПОДОБРЕНИЯ
+    + ново движение: включване на полазването на валути при зареждане на предишни движение с валути
diff --git a/metadata/bg-BG/changelogs/52.txt b/metadata/bg-BG/changelogs/52.txt
new file mode 100644 (file)
index 0000000..9c8c8db
--- /dev/null
@@ -0,0 +1,5 @@
+* ПОПРАВКИ
+    + коригирани версии на gradle
+* ДРУГИ
+    + обновени версии на множество библиотеки
+    + прицелване във версия 31 на платформата
diff --git a/metadata/bg-BG/changelogs/53.txt b/metadata/bg-BG/changelogs/53.txt
new file mode 100644 (file)
index 0000000..427dbb3
--- /dev/null
@@ -0,0 +1,4 @@
+* ПОПРАВКИ
+    + отстранен проблем със съвместимостта с hledger-web 1.23+ при изпращане на нови транзакции. Благодарности на Faye Duxovni за поправката!
+    + поправен срив при изтриване на шаблони
+    + поправен рядък срив при изпращане на транзакции, съдържащи повече от една сметка без сума и нулев остатъчен баланс
diff --git a/metadata/bg-BG/changelogs/54.txt b/metadata/bg-BG/changelogs/54.txt
new file mode 100644 (file)
index 0000000..2e1dace
--- /dev/null
@@ -0,0 +1,2 @@
+* ПОПРАВКИ
+    + отстранен проблем с централизираното резервно копие на настройките
diff --git a/metadata/bg-BG/changelogs/55.txt b/metadata/bg-BG/changelogs/55.txt
new file mode 100644 (file)
index 0000000..208db92
--- /dev/null
@@ -0,0 +1,2 @@
+* ПОПРАВКИ
+    + отстранен проблем при изпращане на транзакции към hledger-web 1.23+
diff --git a/metadata/bg-BG/changelogs/56.txt b/metadata/bg-BG/changelogs/56.txt
new file mode 100644 (file)
index 0000000..218ba4a
--- /dev/null
@@ -0,0 +1,4 @@
+* ПОПРАВКИ
+    + Позволяване на потребителски сертификати в настройките за сигурността на мрежовите връзки
+* ДРУГИ
+    + Обновена версия на gradle
index d5ca2e576b8871864849b99eb60f44c3b6f1fd97..b145702319cba17883f1d211bdafecbabcefb055 100644 (file)
@@ -2,11 +2,11 @@
 
 hledger-web е уеб интерфейс за hledger - система за двустранно счетоводство, базирана на текстови файлове.
 
 
 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>Списък на сметките с текущо салдо, включително в няколко валути</li><li>Списък на движенията по сметките с филтър по име на сметка</li><li>Въвеждане на ново движение по сметка</li><li>Множество източници на данни, в цвят по желание</li><li>Идентифициране пред източника на данни</li></ul>
+<ul><li>Списък на сметките с текущо салдо</li><li>Списък на движенията по сметките с филтър по име на сметка</li><li>Въвеждане на ново движение по сметка</li><li>Работа с валути</li><li>Забележки към движенията по сметки и към отделни пера</li><li>Множество източници на данни, в цвят по желание</li><li>Макети на движения по сметки, активирани чрез сканиране на 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а Ð·Ð° Ð½Ð¾Ð²Ð¾ Ð´Ð²Ð¸Ñ\88ение Ð¿Ð¾ Ñ\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>
diff --git a/metadata/bg-BG/images/icon.png b/metadata/bg-BG/images/icon.png
new file mode 100644 (file)
index 0000000..9f49c28
Binary files /dev/null and b/metadata/bg-BG/images/icon.png differ
index 394c9aba45d94ef16cfa92ef5045ecdc7ddee9a1..fae6961480b3e6c1edfe7f2c452effb41b10aab6 100644 (file)
Binary files a/metadata/bg-BG/images/phoneScreenshots/drawer-open.png and b/metadata/bg-BG/images/phoneScreenshots/drawer-open.png differ
index 56e7ec48ffe34de5dab8c9c41e0213a3b957e18f..829160c8d1b39dc6f40c28e2d76269db78dece66 100644 (file)
@@ -1,6 +1,6 @@
  * NEW:
   - App shortcuts for starting the new transaction activity on Android 7.1+
  * NEW:
   - App shortcuts for starting the new transaction activity on Android 7.1+
-  - Auto-filling of the accounts in the new transaction screen can be limitted to the transactions using accounts corresponding to a filter -- the filter is set in the profile details
+  - Auto-filling of the accounts in the new transaction screen can be limited to the transactions using accounts corresponding to a filter -- the filter is set in the profile details
  * IMPROVED:
   - Account list: Accounts with many commodities have their commodity list collapsed to avoid filling too much of the screen with one account
   - Account list: Viewing account's transactions migrated to a context menu
  * IMPROVED:
   - Account list: Accounts with many commodities have their commodity list collapsed to avoid filling too much of the screen with one account
   - Account list: Viewing account's transactions migrated to a context menu
index a55f91cd7614ab1fb59b7725834f3ef48e693052..e064b67b160e2b3831bf7388930e3aedfa4fda0d 100644 (file)
@@ -3,7 +3,7 @@
 * SECURITY
     + avoid exposing basic HTTP authentication to wifi portals
     + profile editor: warn when using authentication with insecure HTTP scheme
 * SECURITY
     + avoid exposing basic HTTP authentication to wifi portals
     + profile editor: warn when using authentication with insecure HTTP scheme
-    + permit cleartext HTTP traffic on Android 8+ (still, please use HTTPS to keep yout data safe while in transit)
+    + permit cleartext HTTP traffic on Android 8+ (still, please use HTTPS to keep your data safe while in transit)
 * IMPROVEMENTS
     + clarify that crash reports are sent via email and user can review them before sending
     + allow toggling password visibility in profile details
 * IMPROVEMENTS
     + clarify that crash reports are sent via email and user can review them before sending
     + allow toggling password visibility in profile details
diff --git a/metadata/en-US/changelogs/31.txt b/metadata/en-US/changelogs/31.txt
new file mode 100644 (file)
index 0000000..fe8ad05
--- /dev/null
@@ -0,0 +1,16 @@
+Change log: <https://mole.ktnx.net/#index6h1>
+
+* NEW
+    + account-level comments and currency/commodity support for new transactions
+    + controllable entry dates in the future
+    + support 1.14 and 1.15+ JSON API
+* IMPROVEMENTS
+    + darker yellow, green and cyan theme colours
+    + suggest distinct color for new profiles
+    + improved profile editor interface
+    + avoid UI lockup while looking for a previous transaction with the chosen description
+* FIXES
+    + restore ability to scroll the profile details screen
+    + remove profile-specific options from the database when removing a profile
+    + fixed stuck refreshing indicator when main view is slid to the transaction list while transactions are loading
+    + limit the number of launcher shortcuts to the maximum supported
diff --git a/metadata/en-US/changelogs/32.txt b/metadata/en-US/changelogs/32.txt
new file mode 100644 (file)
index 0000000..d37e9c6
--- /dev/null
@@ -0,0 +1,7 @@
+* NEW
+    + transaction-level comment entry
+    + ability to hide comment entry, per profile
+* FIXES:
+    + fixed crash when parsing posting flags with hledger-web before 1.14
+    + visual fixes
+    + fix numerical entry with some Samsung keyboards
diff --git a/metadata/en-US/changelogs/33.txt b/metadata/en-US/changelogs/33.txt
new file mode 100644 (file)
index 0000000..65cc0b4
--- /dev/null
@@ -0,0 +1 @@
+* Additional, universal fix for entering numbers
diff --git a/metadata/en-US/changelogs/34.txt b/metadata/en-US/changelogs/34.txt
new file mode 100644 (file)
index 0000000..2072184
--- /dev/null
@@ -0,0 +1,10 @@
+* NEW
+    + show transaction-level comment in transaction list
+    + scroll to a specific date in the transaction list
+* IMPROVEMENTS
+    + better all-around theme integration; employ some material design recommendations
+    + follow system-wide font size settings
+* FIXES
+    + fix a crash in legacy parser with amounts like '$123.45'
+    + fix a crash upon profile theme change
+    + various small fixes
diff --git a/metadata/en-US/changelogs/35.txt b/metadata/en-US/changelogs/35.txt
new file mode 100644 (file)
index 0000000..4f6dc9a
--- /dev/null
@@ -0,0 +1,4 @@
+* IMPROVEMENTS
+    + better theme support, especially in system-wide dark mode
+* FIXES
+    + restore f-droid listing icon
diff --git a/metadata/en-US/changelogs/36.txt b/metadata/en-US/changelogs/36.txt
new file mode 100644 (file)
index 0000000..714ede5
--- /dev/null
@@ -0,0 +1,13 @@
+
+* NEW
+    + splash screen on startup
+    + show account/transaction counts
+* IMPROVEMENTS
+    + theme fixes, improved contrast
+    + better responsiveness, more work moved to background threads
+    + faster storage of retrieved data
+    + last update info moved to lists to save space
+* FIXES
+    + fixed progress of data retrieval from hledger-web
+    + fixed extra fetches of remote data
+    + fill currency list with data from the journal
diff --git a/metadata/en-US/changelogs/37.txt b/metadata/en-US/changelogs/37.txt
new file mode 100644 (file)
index 0000000..b4e5a5b
--- /dev/null
@@ -0,0 +1,10 @@
+
+* 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
diff --git a/metadata/en-US/changelogs/38.txt b/metadata/en-US/changelogs/38.txt
new file mode 100644 (file)
index 0000000..d974994
--- /dev/null
@@ -0,0 +1,7 @@
+
+* 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
diff --git a/metadata/en-US/changelogs/39.txt b/metadata/en-US/changelogs/39.txt
new file mode 100644 (file)
index 0000000..e3d936c
--- /dev/null
@@ -0,0 +1,3 @@
+
+* FIXES
+    + fix a bug in db migration for profiles without detected version
diff --git a/metadata/en-US/changelogs/40.txt b/metadata/en-US/changelogs/40.txt
new file mode 100644 (file)
index 0000000..7d7be37
--- /dev/null
@@ -0,0 +1,11 @@
+
+* 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
diff --git a/metadata/en-US/changelogs/41.txt b/metadata/en-US/changelogs/41.txt
new file mode 100644 (file)
index 0000000..4efed71
--- /dev/null
@@ -0,0 +1,12 @@
+
+* 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
diff --git a/metadata/en-US/changelogs/42.txt b/metadata/en-US/changelogs/42.txt
new file mode 100644 (file)
index 0000000..bff8da5
--- /dev/null
@@ -0,0 +1,4 @@
+* 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
diff --git a/metadata/en-US/changelogs/43.txt b/metadata/en-US/changelogs/43.txt
new file mode 100644 (file)
index 0000000..5e16021
--- /dev/null
@@ -0,0 +1,2 @@
+* FIXES
+    + fix auto-completion of transaction names with non-ASCII characters on some Android variants/versions (broken in 0.18.0)
diff --git a/metadata/en-US/changelogs/44.txt b/metadata/en-US/changelogs/44.txt
new file mode 100644 (file)
index 0000000..c2c4167
--- /dev/null
@@ -0,0 +1,4 @@
+* NEW
+    + backup/restore of profile/template configuration to a file
+* FIXES
+    + fix a couple of crashes related to starting new transaction via shortcut
diff --git a/metadata/en-US/changelogs/45.txt b/metadata/en-US/changelogs/45.txt
new file mode 100644 (file)
index 0000000..d83be7d
--- /dev/null
@@ -0,0 +1,6 @@
+* 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
diff --git a/metadata/en-US/changelogs/46.txt b/metadata/en-US/changelogs/46.txt
new file mode 100644 (file)
index 0000000..57f40b7
--- /dev/null
@@ -0,0 +1,4 @@
+* NEW
+    + cloud backup
+* FIXES
+    + two database problems fixed, one causing crashes at startup
diff --git a/metadata/en-US/changelogs/47.txt b/metadata/en-US/changelogs/47.txt
new file mode 100644 (file)
index 0000000..28581c0
--- /dev/null
@@ -0,0 +1,2 @@
+* FIXES
+    + another fix to DB migration from v0.16.0
diff --git a/metadata/en-US/changelogs/48.txt b/metadata/en-US/changelogs/48.txt
new file mode 100644 (file)
index 0000000..f0ce53d
--- /dev/null
@@ -0,0 +1,4 @@
+* KNOWN PROBLEMS
+    + Incompatibility with hledger-web 1.23+
+* FIXES
+    + fix auto-completion of transaction description
diff --git a/metadata/en-US/changelogs/49.txt b/metadata/en-US/changelogs/49.txt
new file mode 100644 (file)
index 0000000..422c2ce
--- /dev/null
@@ -0,0 +1,4 @@
+* NEW
+    + Add support for hledger-web 1.23
+* FIXES
+    + Ship database support file missed in v0.20.4
diff --git a/metadata/en-US/changelogs/50.txt b/metadata/en-US/changelogs/50.txt
new file mode 100644 (file)
index 0000000..797fcc1
--- /dev/null
@@ -0,0 +1,4 @@
+* 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
diff --git a/metadata/en-US/changelogs/51.txt b/metadata/en-US/changelogs/51.txt
new file mode 100644 (file)
index 0000000..379834e
--- /dev/null
@@ -0,0 +1,6 @@
+* 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
diff --git a/metadata/en-US/changelogs/52.txt b/metadata/en-US/changelogs/52.txt
new file mode 100644 (file)
index 0000000..13f6c20
--- /dev/null
@@ -0,0 +1,6 @@
+* FIXES
+    + sync gradle version requirements
+* OTHERS
+    + bump version of several dependent libraries
+    + bump SDK version to 31
+    + adjust deprecated constructor usage
diff --git a/metadata/en-US/changelogs/53.txt b/metadata/en-US/changelogs/53.txt
new file mode 100644 (file)
index 0000000..8e8601b
--- /dev/null
@@ -0,0 +1,4 @@
+* 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
diff --git a/metadata/en-US/changelogs/54.txt b/metadata/en-US/changelogs/54.txt
new file mode 100644 (file)
index 0000000..a2e3a20
--- /dev/null
@@ -0,0 +1,2 @@
+* FIXES
+    + fix cloud backup
diff --git a/metadata/en-US/changelogs/55.txt b/metadata/en-US/changelogs/55.txt
new file mode 100644 (file)
index 0000000..3697ab5
--- /dev/null
@@ -0,0 +1,2 @@
+* FIXES:
+    + fixed sending of transactions to hledger-web 1.23+
diff --git a/metadata/en-US/changelogs/56.txt b/metadata/en-US/changelogs/56.txt
new file mode 100644 (file)
index 0000000..9b7d3ac
--- /dev/null
@@ -0,0 +1,4 @@
+* FIXES:
+    + allow user certificates in network security config
+* OTHERS:
+    + bump gradle version
index 4eca9bf3ac23436837dbaa10cd0bd953e0685830..3e9f3c28bc8841097aa84ecfaa7276b41a58d82b 100644 (file)
@@ -6,7 +6,7 @@ MoLe (from "Mobile Ledger") is a convenient front-end to hledger-web, providing
 
 Features:
 
 
 Features:
 
-<ul><li>List of accounts with their current balance, including support for multiple currencies</li><li>Transaction list with filters</li><li>Input of new transactions</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:
 
 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>
diff --git a/metadata/en-US/images/icon.png b/metadata/en-US/images/icon.png
new file mode 100644 (file)
index 0000000..9f49c28
Binary files /dev/null and b/metadata/en-US/images/icon.png differ
index 394c9aba45d94ef16cfa92ef5045ecdc7ddee9a1..fae6961480b3e6c1edfe7f2c452effb41b10aab6 100644 (file)
Binary files a/metadata/en-US/images/phoneScreenshots/drawer-open.png and b/metadata/en-US/images/phoneScreenshots/drawer-open.png differ
index 60b01a6325a7cd849135051c0c9894fba4986694..64cebf27e9e8ab4d72652f6ecd735d2baa20f391 100644 (file)
 #!/usr/bin/perl
 
 #!/usr/bin/perl
 
-use strict; use warnings; use utf8;
+use strict; use warnings; use utf8::all;
 use autodie;
 use autodie;
-use Math::Trig;
-use File::Basename qw(basename dirname);
-use File::Temp qw(tempfile);
+use Carp;
+
+use Type::Tiny;
+use Types::Standard qw(StrictNum);
+my $colorValue = Type::Tiny->new(
+    parent     => StrictNum,
+    constraint => '($_ >= 0) and ($_ <= 1)'
+);
+
+package Color::HSL;
+use Moo;
+use namespace::clean;
+
+has h => ( is => 'ro', isa => $colorValue );
+has s => ( is => 'ro', isa => $colorValue );
+has l => ( is => 'ro', isa => $colorValue );
+
+package Color::sRGB;
+use Moo;
+use Types::Standard qw(StrictNum);
+use List::MoreUtils qw(minmax);
+
+use namespace::clean;
+
+use overload '""' => 'hexTuple';
+
+has r => ( is => 'ro', isa => $colorValue );
+has g => ( is => 'ro', isa => $colorValue );
+has b => ( is => 'ro', isa => $colorValue );
 
 sub hexTuple {
 
 sub hexTuple {
-       my ($r, $g, $b) = @_;
-       return sprintf('%02x%02x%02x', int(255*$r+0.5), int(255*$g+0.5), int(255*$b+0.5));
+    my $self = shift;
+    return sprintf( '%02x%02x%02x',
+        int( $self->r * 255 + 0.5 ),
+        int( $self->g * 255 + 0.5 ),
+        int( $self->b * 255 + 0.5 ) );
 }
 }
-sub hsvHex {
-       my ($hue, $sat, $val ) = @_;
-       my $h = int($hue * 6);
-       my $f = $hue * 6 - $h;
-       my $p = $val * (1 - $sat);
-       my $q = $val * ( 1 - $f * $sat);
-       my $t = $val * ( 1 - (1-$f) * $sat);
 
 
-       return hexTuple($val, $t, $p) if $h == 0 or $h == 6;
-       return hexTuple($q, $val, $p) if $h == 1;
-       return hexTuple($p, $val, $t) if $h == 2;
-       return hexTuple($p, $q, $val) if $h == 3;
-       return hexTuple($t, $p, $val) if $h == 4;
-       return hexTuple($val, $p, $q) if $h == 5;
+# https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
+sub _norm {
+    return $_[0] / 12.92 if $_[0] <= 0.03928;
+    return ( ( $_[0] + 0.055 ) / 1.055 )**2.4;
+}
 
 
-       die $h;
+sub relativeLuminance {
+    my $self = shift;
+
+    return 0.2126 * _norm( $self->r ) + 0.7152 * _norm( $self->g )
+        + 0.0722 * _norm( $self->b );
 }
 
 }
 
-# https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
-sub hslHex {
-       my ($hue, $sat, $lig ) = @_;
-       $hue = $hue / 360.0;
-       my $h = ($hue * 6.0);
-       my $c = (1 - abs(2.0*$lig - 1)) * $sat;
-       my $h_mod_2 = $h - 2.0*int($h/2);
-       my $x = $c * (1 - abs($h_mod_2 - 1));
-       my ($r, $g, $b);
-       my $m = $lig - $c / 2.0;
-
-       return hexTuple($c + $m, $x + $m,  0 + $m) if $h < 1 or $h == 6;
-       return hexTuple($x + $m, $c + $m,  0 + $m) if $h < 2;
-       return hexTuple( 0 + $m, $c + $m, $x + $m) if $h < 3;
-       return hexTuple( 0 + $m, $x + $m, $c + $m) if $h < 4;
-       return hexTuple($x + $m,  0 + $m, $c + $m) if $h < 5;
-       return hexTuple($c + $m,  0 + $m, $x + $m) if $h < 6;
+sub BLACK {
+    shift->new( r => 0, g => 0, b => 0 );
+}
 
 
-       die $h;
+sub WHITE {
+    shift->new( r => 1, g => 1, b => 1 );
 }
 
 my @hexDigit = split //, '0123456789abcdef';
 }
 
 my @hexDigit = split //, '0123456789abcdef';
-my %hexValue = map(
-               (lc($hexDigit[$_]) => $_, uc($hexDigit[$_]) => $_ ),
-               0..15 );
+my %hexValue =
+    map( ( lc( $hexDigit[$_] ) => $_, uc( $hexDigit[$_] ) => $_ ), 0 .. 15 );
 
 
-sub min {
-       my $min = shift;
+sub fromHexTriplet {
+    my ( $class, $triplet ) = @_;
 
 
-       for (@_) { $min = $_ if $_ < $min }
+    my @d = $triplet =~ /^#?(.)(.)(.)(.)(.)(.)$/
+        or die "'$triplet' is not a valid colour triplet";
 
 
-       return $min;
+    return $class->new(
+        r => ( 16 * $hexValue{ $d[0] } + $hexValue{ $d[1] } ) / 255.0,
+        g => ( 16 * $hexValue{ $d[2] } + $hexValue{ $d[3] } ) / 255.0,
+        b => ( 16 * $hexValue{ $d[4] } + $hexValue{ $d[5] } ) / 255.0
+    );
 }
 
 }
 
-sub max {
-       my $max = shift;
+# https://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
+sub fromHSL {
+    my ( $class, $hsl ) = @_;
+    my $hue = $hsl->h;
+    my $sat = $hsl->s;
+    my $lig = $hsl->l;
+
+    my $h       = ( $hue * 6.0 );
+    my $c       = ( 1 - abs( 2.0 * $lig - 1 ) ) * $sat;
+    my $h_mod_2 = $h - 2.0 * int( $h / 2 );
+    my $x       = $c * ( 1 - abs( $h_mod_2 - 1 ) );
+    my ( $r, $g, $b );
+    my $m = $lig - $c / 2.0;
+
+    return $class->new( r => $c + $m, g => $x + $m, b => 0 + $m )
+        if $h < 1 or $h == 6;
+    return $class->new( r => $x + $m, g => $c + $m, b => 0 + $m )  if $h < 2;
+    return $class->new( r => 0 + $m,  g => $c + $m, b => $x + $m ) if $h < 3;
+    return $class->new( r => 0 + $m,  g => $x + $m, b => $c + $m ) if $h < 4;
+    return $class->new( r => $x + $m, g => 0 + $m,  b => $c + $m ) if $h < 5;
+    return $class->new( r => $c + $m, g => 0 + $m,  b => $x + $m ) if $h < 6;
+
+    die $h;
+}
+
+sub toHSL {
+    my $self = shift;
+
+    my ( $m, $M ) = minmax( $self->r, $self->g, $self->b );
 
 
-       for (@_) { $max = $_ if $_ > $max }
+    my $C = $M - $m;
 
 
-       return $max;
+    my $h;
+    if ( $C == 0 ) {
+        $h = 0;
+    }
+    elsif ( $self->r == $M ) {
+        $h = ( $self->g - $self->b ) / $C;
+        $h -= 6 * int( $h / 6.0 );
+    }
+    elsif ( $self->g == $M ) {
+        $h = ( $self->b - $self->r ) / $C + 2;
+    }
+    elsif ( $self->b == $M ) {
+        $h = ( $self->r - $self->g ) / $C + 4;
+    }
+    else { die "$C, $M, $self"; }
+
+    my $H = 60 * $h;
+    my $L = ( $M + $m ) / 2;
+
+    my $S = ( $L <= 0.5 ) ? $C / ( 2 * $L ) : $C / ( 2 - 2 * $L );
+
+    return Color::HSL->new( h => $H/360.0, s => $S, l => $L );
 }
 
 }
 
-sub hexToRGB {
-       my $hexTriplet = shift;
+sub contrastWith {
+    my ( $self, $ref ) = @_;
 
 
-       my @d = $hexTriplet =~ /^#?(.)(.)(.)(.)(.)(.)/;
+    my $myL = $self->relativeLuminance;
+    my $refL = $ref->relativeLuminance;
 
 
-       return (16 * $hexValue{$d[0]} + $hexValue{$d[1]},
-               16 * $hexValue{$d[2]} + $hexValue{$d[3]},
-               16 * $hexValue{$d[4]} + $hexValue{$d[5]});
+    my $ratio = ( $myL + 0.05 ) / ( $refL + 0.05 );
+    $ratio = 1 / $ratio if $ratio < 1;
+    return $ratio;
 }
 
 }
 
-sub hexToHSL {
-       my $hexTriplet = shift;
+package MAIN;
 
 
-       my ($r,$g,$b) = hexToRGB($hexTriplet);
-       warn "$hexTriplet -> $r:$g:$b";
+use Math::Trig;
+use File::Basename qw(basename dirname);
+use File::Temp qw(tempfile);
+use Getopt::Long;
 
 
-       for ($r, $g, $b ) { $_ = $_ / 255.0 }
+my $opt_night;
 
 
-       my $M = max($r, $g, $b);
-       my $m = min($r, $g, $b);
-       my $C = $M - $m;
+GetOptions(
+    'night!'    => \$opt_night,
+) or exit 1;
 
 
-       my $h;
-       if ($C == 0) {
-               $h = 0;
-       }
-       elsif ( $r == $M ) {
-               $h = ($g-$b)/$C;
-               $h -= 6*int($h/6.0);
-       }
-       elsif ( $g == $M ) {
-               $h = ($b-$r)/$C + 2;
-       }
-       elsif ( $b == $M ) {
-               $h = ($r-$g)/$C + 4;
-       }
-       else { die "$C, $M, $r, $g, $b"; }
+my $DEFAULT_HUE = 261.2245;
 
 
-       my $H = 60 * $h;
-       my $L = ($M + $m) / 2;
+sub hexTuple {
+       my ($r, $g, $b) = @_;
+       return sprintf('%02x%02x%02x', int(255*$r+0.5), int(255*$g+0.5), int(255*$b+0.5));
+}
+sub hsvHex {
+       my ($hue, $sat, $val ) = @_;
+       my $h = int($hue * 6);
+       my $f = $hue * 6 - $h;
+       my $p = $val * (1 - $sat);
+       my $q = $val * ( 1 - $f * $sat);
+       my $t = $val * ( 1 - (1-$f) * $sat);
 
 
-       my $S = ( $L <= 0.5 ) ? $C/(2*$L) : $C / (2-2*$L);
+       return hexTuple($val, $t, $p) if $h == 0 or $h == 6;
+       return hexTuple($q, $val, $p) if $h == 1;
+       return hexTuple($p, $val, $t) if $h == 2;
+       return hexTuple($p, $q, $val) if $h == 3;
+       return hexTuple($t, $p, $val) if $h == 4;
+       return hexTuple($val, $p, $q) if $h == 5;
 
 
-       return( $H, $S, $L );
+       die $h;
 }
 
 }
 
-my $baseColorHSV = [ hexToHSL('#935ff2') ];
-my $baseColorHue = $baseColorHSV->[0];
-warn sprintf( 'H:%1.4f S:%1.4f V:%1.4f', @$baseColorHSV );
-warn sprintf( 'H:%1.4f S:%1.4f L:%1.4f', hexToHSL('#3e148c') );
-my @target = hexToRGB('#935ff2');
-my ($best, $min_dist);
-for (my $s = 0.50; $s < 0.90; $s += 0.001) {
-       for ( my $l = 0.50; $l <= 0.80; $l += 0.001 ) {
-               my $hexColor = hslHex($baseColorHue, $s, $l);
-               my ($r,$g,$b) = hexToRGB( $hexColor );
-               my $dist = abs($r-$target[0])
-                        + abs($g-$target[1])
-                        + abs($b-$target[2]);
-               if (not defined($best) or $dist < $min_dist) {
-                       $best = [ $s, $l, $hexColor ];
-                       $min_dist = $dist;
-               }
-       }
+sub hslHex {
+    my ( $h, $s, $l ) = @_;
+    return Color::sRGB->fromHSL(
+        Color::HSL->new( { h => $h / 360.0, s => $s, l => $l } ) )->hexTuple;
 }
 }
-warn sprintf( 's%1.3f, l%1.3f -> %s',
-       @$best );
 
 
-my $baseTheme = "AppTheme.NoActionBar";
+warn sprintf("%s: %2.1f\n", 'white', Color::sRGB->WHITE->relativeLuminance);
+warn sprintf("%s: %2.1f\n", 'black', Color::sRGB->BLACK->relativeLuminance);
+warn sprintf( "%s: %2.1f\n",
+    '50% gray',
+    Color::sRGB->new( r => 0.5, g => 0.5, b => 0.5 )->relativeLuminance );
+
+my $baseColor = '#935ff2';
+my $baseColorRGB = Color::sRGB->fromHexTriplet($baseColor);
+my $baseColorHSL = $baseColorRGB->toHSL;
+my $baseColorHue = $baseColorHSL->h;
+warn sprintf(
+    '%s → H:%1.4f S:%1.4f L:%1.4f (luminance: %1.4f; cW: %1.4f, cB: %1.4f)',
+    $baseColor,
+    360 * $baseColorHSL->h,
+    $baseColorHSL->s,
+    $baseColorHSL->l,
+    $baseColorRGB->relativeLuminance,
+    $baseColorRGB->contrastWith( Color::sRGB->WHITE ),
+    $baseColorRGB->contrastWith( Color::sRGB->BLACK ),
+);
+# # find best saturation/lightness for the desired color
+# # test if the above is correct
+# my ($best, $min_dist);
+# for (my $s = 0.50; $s < 0.90; $s += 0.001) {
+#     for ( my $l = 0.50; $l <= 0.80; $l += 0.001 ) {
+#         my $color = Color::sRGB->fromHSL(
+#             Color::HSL->new( h => $baseColorHue, s => $s, l => $l ) );
+#         my $dist =
+#               abs( $color->r - $baseColorRGB->r )
+#             + abs( $color->g - $baseColorRGB->g )
+#             + abs( $color->b - $baseColorRGB->b );
+#         if ( not defined($best) or $dist < $min_dist ) {
+#             $best     = [ $s, $l, $color ];
+#             $min_dist = $dist;
+#         }
+#     }
+# }
+# warn sprintf( 's%1.3f, l%1.3f → %s', @$best );
 
 
-use constant STEP_DEGREES => 5;
+my $baseTheme = "AppTheme";
 
 
-# # hsb
-# for( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
-#      printf "<style name=\"%s.%03d\" parent=\"%s\">\n",
-#              $baseTheme, $hue, $baseTheme;
-#      printf "  <item name=\"colorPrimary\">#%s</item>\n",
-#                      hsvHex($hue/360.0, 0.61, 0.95);
-#      printf "  <item name=\"colorPrimaryDark\">#%s</item>\n",
-#                      hsvHex($hue/360.0, 0.86, 0.55);
-#      printf "  <item name=\"colorAccent\">#%s</item>\n",
-#                      hsvHex(($hue-4)/360.0, 0.72, 0.82);
-#      printf "  <item name=\"drawer_background\">#ffffffff</item>\n";
-#      printf "  <item name=\"table_row_dark_bg\">#28%s</item>\n",
-#                      hsvHex($hue/360.0, 0.65, 0.83);
-#      printf "  <item name=\"table_row_light_bg\">#28%s</item>\n",
-#                      hsvHex($hue/360.0, 0.20, 1.00);
-#      printf "  <item name=\"header_border\">#80%s</item>\n",
-#                      hsvHex(($hue+6)/360.0, 0.86, 0.55);
-#      printf "</style>\n";
-# }
+use constant STEP_DEGREES => 5;
 
 
-# HSL
 sub outputThemes {
 sub outputThemes {
-       my $out = shift;
-       my $baseIndent = shift;
-       $out->print(hslStyleForHue($baseColorHue, undef, $baseIndent));
-       for( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
-               $out->print("\n");
-               $out->print(hslStyleForHue($hue, $baseTheme, $baseIndent));
-       }
+    my $out        = shift;
+    my $baseIndent = shift;
+    $out->print("\n");
+    $out->print(
+        hslStyleForHue( $DEFAULT_HUE, $baseTheme, $baseIndent, 'default' ) );
+    for ( my $hue = 0; $hue < 360; $hue += STEP_DEGREES ) {
+        $out->print("\n");
+        $out->print( hslStyleForHue( $hue, $baseTheme, $baseIndent ) );
+    }
+}
+
+sub bestLightnessForHue {
+    my ( $h, $s ) = @_;
+    my $targetContrast = $opt_night ? 5.16 : 4.07;
+    my $white = $opt_night ? Color::sRGB->BLACK : Color::sRGB->WHITE;
+    my $bestLightness;
+    my $bestContrast;
+    for ( my $l = 0; $l < 1; $l += 0.002 ) {
+        my $contrast = Color::sRGB->fromHSL(
+            Color::HSL->new( { h => $h, s => $s, l => $l } ) )
+            ->contrastWith($white);
+
+        if ( defined $bestLightness ) {
+            if (abs( $contrast - $targetContrast ) <
+                abs( $bestContrast - $targetContrast ) )
+            {
+                $bestLightness = $l;
+                $bestContrast  = $contrast;
+            }
+        }
+        else {
+            $bestLightness = $l;
+            $bestContrast = $contrast;
+        }
+    }
+
+    warn sprintf(
+        "Found best lightness for hue %1.4f: %1.4f (contrast %1.4f)\n",
+        360 * $h, $bestLightness, $bestContrast );
+    return $bestLightness;
 }
 
 sub hslStyleForHue {
        my $hue = shift;
        my $base = shift;
        my $baseIndent = shift // '';
 }
 
 sub hslStyleForHue {
        my $hue = shift;
        my $base = shift;
        my $baseIndent = shift // '';
-
-       my $blueL = 0.665;
-       my $yellowL = 0.350;
-
-       my $blueL2 = 0.350;
-       my $yellowL2 = 0.500;
-
-       # $y == 0 for yellow
-       my $y = $hue - 60;
-       $y += 360 if $y < 0;
-       # $q == 0 for yellow, 1 for blue
-       my $q = cos(deg2rad(abs($y-180)/2.0));
-       my $l1 = $yellowL + ($blueL - $yellowL) * $q;
-       my $l2 = 0.250 + 0.250 * $q;
-       my $l3 = 0.950;
-       my $l4 = 0.980;
+        my $subTheme = shift // sprintf('%03d', $hue);
+
+       my %lQ = (
+               0   => 0.550,   # red
+               60  => 0.250,   # yellow
+               120 => 0.290,   # green
+               180 => 0.300,   # cyan
+               240 => 0.680,   # blue
+               300 => 0.450,   # magenta
+       );
+       $lQ{360} = $lQ{0};
+
+       my ($x0, $x1, $y0, $y1);
+       $x0 = (int( $hue / 60 ) * 60) % 360;
+       $x1 = $x0 + 60;
+       $y0 = $lQ{$x0};
+       $y1 = $lQ{$x1};
+
+        my $S = 0.8497;
+
+       # linear interpolation
+        #my $l1 = $y0 + 1.0 * ( $hue - $x0 ) * ( $y1 - $y0 ) / ( $x1 - $x0 );
+        my $l1 = bestLightnessForHue( $hue / 360.0, $S );
+        #$l1 += ( 1 - $l1 ) * 0.20 if $opt_night;
+
+        #my $l2 = $opt_night ? ( $l1 + ( 1 - $l1 ) * 0.15 ) : $l1 * 0.85;
+        my $l2 = $l1 * 0.80;
+        my $l3 = $opt_night ? 0.150                        : 0.950;
+        my $l4 = $opt_night ? 0.100                        : 0.980;
 
        my $result = "";
        my $indent = "$baseIndent    ";
 
        if ($base) {
 
        my $result = "";
        my $indent = "$baseIndent    ";
 
        if ($base) {
-               $result .= sprintf "$baseIndent<style name=\"%s.%03d\" parent=\"%s\">\n",
-                        $baseTheme, $hue, $baseTheme;
+               $result .= sprintf "$baseIndent<style name=\"%s.%s\" parent=\"%s\">\n",
+                        $baseTheme, $subTheme, $baseTheme;
         }
         else {
                 $result .= sprintf "$baseIndent<style name=\"%s\">\n",
                         $baseTheme;
         }
         else {
                 $result .= sprintf "$baseIndent<style name=\"%s\">\n",
                         $baseTheme;
-                $result .= "$indent<item name=\"windowActionBar\">false</item>\n";
-                $result .= "$indent<item name=\"windowNoTitle\">true</item>\n";
-                $result .= "$indent<item name=\"textColor\">#8a000000</item>\n";
+#                $result .= "$indent<item name=\"windowActionBar\">false</item>\n";
+#                $result .= "$indent<item name=\"windowNoTitle\">true</item>\n";
+#                $result .= "$indent<item name=\"textColor\">#757575</item>\n";
         }
         }
-        my $S = 0.845;
-        $result .= sprintf "$indent<item name=\"colorPrimary\">#%s</item>\n",
-                hslHex($hue, $S, $l1);
-        $result .= sprintf "$indent<item name=\"colorPrimaryTransparent\">#00%s</item>\n",
-                hslHex($hue, $S, $l1);
-        $result .= sprintf "$indent<item name=\"colorAccent\">#%s</item>\n",
-                hslHex($hue, $S, $l2);
-        $result .= "$indent<item name=\"drawer_background\">#ffffffff</item>\n";
-        $result .= sprintf "$indent<item name=\"table_row_dark_bg\">#%s</item>\n",
-                hslHex($hue, $S, $l3);
-        $result .= sprintf "$indent<item name=\"table_row_light_bg\">#%s</item>\n",
-                hslHex($hue, $S, $l4);
+
+        $result .= sprintf "$indent<!-- h: %1.4f s:%1.4f l:%1.4f -->\n", $hue,
+            $S, $l1 if 0;
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorPrimary', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#00%s</item>\n",
+            'colorPrimaryTransparent', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorSecondary', hslHex( $hue, $S, $l1 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'colorPrimaryDark', hslHex( $hue, $S*0.8, $l2 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'table_row_dark_bg', hslHex( $hue, $S, $l3 );
+        $result .= sprintf "$indent<item name=\"%s\">#%s</item>\n",
+            'table_row_light_bg', hslHex( $hue, $S, $l4 );
         $result .= "$baseIndent</style>\n";
 
         return $result;
         $result .= "$baseIndent</style>\n";
 
         return $result;
@@ -231,6 +348,7 @@ if ($xml) {
        my $start_marker = '<!-- theme list start -->';
        my $end_marker = '<!-- theme list end -->';
        my ($fh, $filename) = tempfile(basename($0).'.XXXXXXXX', DIR => dirname($xml));
        my $start_marker = '<!-- theme list start -->';
        my $end_marker = '<!-- theme list end -->';
        my ($fh, $filename) = tempfile(basename($0).'.XXXXXXXX', DIR => dirname($xml));
+        $fh->binmode(':utf8');
        open(my $in, '<', $xml);
        my $base_indent = '';
        my $state = 'waiting-for-start-marker';
        open(my $in, '<', $xml);
        my $base_indent = '';
        my $state = 'waiting-for-start-marker';
index cd7ee6efa820b1ab8678bfee1af3380a37127577..c796b825b9546229d1a6f2fa111ee54d070041f8 100755 (executable)
@@ -10,11 +10,11 @@ ICON_ART="$ART_DIR/app-icon.svg"
 
 gen_icons() {
     while read size name; do
 
 gen_icons() {
     while read size name; do
-        mkdir -p "$RES_DIR/drawable-$name"
-        DST="$RES_DIR/drawable-$name/app_icon.png"
-        convert -background none "$ICON_ART" -scale ${size}x${size} \
-            -antialias -strip \
-            "$DST"
+        DST_DIR="$RES_DIR/mipmap-$name"
+        mkdir -p "$DST_DIR"
+        DST="$DST_DIR/ic_launcher.png"
+        rsvg-convert --background-color none "$ICON_ART" -w $size -h $size \
+            -o "$DST"
         optipng "$DST"
     done
 }
         optipng "$DST"
     done
 }