From dcc38645c88217fbe6c9a924fb82fb276ec9961f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Fri, 19 Jan 2024 17:47:16 +0100 Subject: [PATCH 1/9] chore: upgrade flutter --- .flutter | 2 +- pubspec.lock | 100 +++++++++++++++++++++++++++------------------------ 2 files changed, 55 insertions(+), 47 deletions(-) diff --git a/.flutter b/.flutter index fed06b3..c1df7d0 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit fed06b31d938f7620ea7417295b8d8d19cf7cf1d +Subproject commit c1df7d07ac60336309bae9dd2d48e7cb8844ec98 diff --git a/pubspec.lock b/pubspec.lock index 30c6e96..e57d14a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "67d591d602906ef9201caf93452495ad1812bea2074f04e25dbd7c133785821b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" build_runner_core: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: code_builder - sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.10.0" collection: dependency: transitive description: @@ -293,10 +293,10 @@ packages: dependency: transitive description: name: file_selector_platform_interface - sha256: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.1" + version: "2.6.2" file_selector_windows: dependency: transitive description: @@ -523,10 +523,10 @@ packages: dependency: transitive description: name: http - sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -547,26 +547,26 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.4" image_picker: dependency: "direct main" description: name: image_picker - sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b" + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.7" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec" + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" url: "https://pub.dev" source: hosted - version: "0.8.9+2" + version: "0.8.9+3" image_picker_for_web: dependency: transitive description: @@ -579,10 +579,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 url: "https://pub.dev" source: hosted - version: "0.8.9" + version: "0.8.9+1" image_picker_linux: dependency: transitive description: @@ -603,10 +603,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "0e827c156e3a90edd3bbe7f6de048b39247b16e58173b08a835b7eb00aba239e" + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b url: "https://pub.dev" source: hosted - version: "2.9.2" + version: "2.9.3" image_picker_windows: dependency: transitive description: @@ -672,18 +672,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" url: "https://pub.dev" source: hosted - version: "9.0.16" + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.1" lints: dependency: transitive description: @@ -712,10 +720,10 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -768,18 +776,18 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_android: dependency: transitive description: @@ -792,10 +800,10 @@ packages: dependency: transitive description: name: path_provider_foundation - sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -808,10 +816,10 @@ packages: dependency: transitive description: name: path_provider_platform_interface - sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" path_provider_windows: dependency: transitive description: @@ -832,10 +840,10 @@ packages: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -848,10 +856,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -864,10 +872,10 @@ packages: dependency: transitive description: name: process - sha256: "266ca5be5820feefc777793d0a583acfc8c40834893c87c00c6c09e2cf58ea42" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" provider: dependency: transitive description: @@ -920,10 +928,10 @@ packages: dependency: transitive description: name: shared_preferences_foundation - sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: @@ -936,10 +944,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: @@ -1157,18 +1165,18 @@ packages: dependency: transitive description: name: web - sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "045ec2137c27bf1a32e6ffa0e734d532a6677bf9016a0d1a406c54e499ff945b" + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.3" webdriver: dependency: transitive description: -- 2.45.2 From 96e672f1d5cbd2cbda8207241dea7b7e4ea74588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 14:41:16 +0100 Subject: [PATCH 2/9] test: create basic tests (#24) Related to #1 but more tests should be added, so not closing Reviewed-on: https://git.mnau.xyz/hernik/prasule/pulls/24 --- .gitignore | 1 + android/app/build.gradle | 6 +- android/build.gradle | 13 --- android/settings.gradle | 14 ++- integration_test/app_test.dart | 140 ++++++++++++++++++++++++ integration_test/setup_test.dart | 24 ---- lib/api/wallet.dart | 82 +++++++++++++- lib/api/wallet_manager.dart | 15 ++- lib/main.dart | 6 +- lib/util/graphs.dart | 1 + lib/views/create_entry.dart | 4 +- lib/views/create_recur_entry.dart | 4 +- lib/views/home.dart | 68 +----------- lib/views/settings/edit_categories.dart | 4 +- lib/views/settings/graph_type.dart | 8 +- lib/views/settings/tessdata_list.dart | 4 +- lib/views/setup.dart | 2 +- pubspec.lock | 4 +- 18 files changed, 276 insertions(+), 124 deletions(-) create mode 100644 integration_test/app_test.dart delete mode 100644 integration_test/setup_test.dart diff --git a/.gitignore b/.gitignore index 24476c5..1a1da39 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +reports \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 7551c3f..b3e80a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -51,7 +51,7 @@ android { applicationId "cafe.caras.prasule" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -76,4 +76,6 @@ flutter { source '../..' } -dependencies {} +dependencies { + implementation 'com.android.support:multidex:1.0.3' +} diff --git a/android/build.gradle b/android/build.gradle index f7eb7f6..bc157bd 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.7.10' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 55c4ca8..1d6d19b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,11 +10,17 @@ pluginManagement { includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + repositories { + google() + mavenCentral() + gradlePluginPortal() } } -include ":app" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} -apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" +include ":app" diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 0000000..fd707c7 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:logger/logger.dart'; +import 'package:prasule/api/category.dart'; +import 'package:prasule/api/entry_data.dart'; +import 'package:prasule/api/wallet.dart'; +import 'package:prasule/api/wallet_entry.dart'; +import 'package:prasule/api/wallet_manager.dart'; + +import 'package:prasule/main.dart'; +import 'package:prasule/pw/platformfield.dart'; + +void main() { + final logger = Logger(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group("Test classes and API", () { + test("Test wallet operations", () async { + expect( + (await WalletManager.listWallets()).length, + equals(0), + ); // check that there are no other wallets + await WalletManager.saveWallet(Wallet.empty); + var w = (await WalletManager.listWallets()).firstOrNull; + expect(w, isNotNull); // check that the wallet was successfully saved + expect(w!.categories.length, equals(1)); + w.categories.add( + WalletCategory( + name: "Testing", + id: w.nextCategoryId, + icon: Icons.abc, + color: Colors.orange, + ), + ); // create test category + final testId = w.nextId; + w.entries.add( + WalletSingleEntry( + data: EntryData(amount: 200, name: "Automated"), + type: EntryType.expense, + date: DateTime.now(), + category: w.categories.last, + id: w.nextId, + ), + ); // create test entry + await WalletManager.saveWallet(w); // save again + w = await WalletManager.loadWallet(w.name); // try loading manually + final e = w.entries.where((element) => element.id == testId).firstOrNull; + expect( + e, + isNotNull, + ); // check that the entry was successfully created + expect( + w.categories.where((element) => element.id == e!.category.id).length, + equals(1), + ); // check that the category exists too + + await WalletManager.deleteWallet(w); + expect( + (await WalletManager.listWallets()).length, + equals(0), + ); + }); + }); + + group("Test app functionality:", () { + testWidgets('First-time setup', (WidgetTester tester) async { + // Delete all data + await WalletManager.deleteAllData(); + // Build our app and trigger a frame. + await tester.pumpWidget( + const MyApp( + locale: Locale('en', 'US'), + ), + ); + await tester.pumpAndSettle(); + logger.i("Looking for welcome header"); + expect(find.text('Welcome!'), findsOneWidget); + + // Tap "Next" button + await tester.tap(find.text("Next")); + await tester.pumpAndSettle(); + + logger.i("Next view, looking for name+balance fields"); + + final firstFields = find.byType(PlatformField); + + expect(firstFields, findsExactly(2)); + + logger.i("Entering text"); + await tester.enterText(find.byType(PlatformField).first, "Debugging"); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(PlatformField).last, "100"); + await tester.pumpAndSettle(); + + // Tap "Next" button + await tester.tap(find.text("Next")); + await tester.pumpAndSettle(); + + // Tap "Finish" button + await tester.tap(find.text("Finish")); + await tester.pumpAndSettle(); + + expect( + find.byWidgetPredicate( + (widget) => + widget is DropdownButton && + ((widget as DropdownButton).value ?? -1) == 0, + ), + findsOne, + ); + }); + + testWidgets('Test rendering of entries', (WidgetTester tester) async { + // Delete all data + await WalletManager.deleteAllData(); + expect((await WalletManager.listWallets()).length, equals(0)); + + // Create test data + final w = Wallet.empty; + await w.createTestEntries(); + expect((await WalletManager.listWallets()).length, equals(1)); + + // Build our app and trigger a frame. + await tester.pumpWidget( + const MyApp( + locale: Locale('en', 'US'), + ), + ); + await tester.pumpAndSettle(); + + // TODO: better test + + expect( + find.byType(ListTile, skipOffstage: false), + findsAtLeast(10), + ); + }); + }); +} diff --git a/integration_test/setup_test.dart b/integration_test/setup_test.dart deleted file mode 100644 index db9ca13..0000000 --- a/integration_test/setup_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:prasule/main.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group("Test Setup screen:", () { - testWidgets('First-time setup', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - expect(find.text('Welcome!'), findsOneWidget); - - // // Tap the '+' icon and trigger a frame. - // await tester.tap(find.byIcon(Icons.add)); - // await tester.pump(); - - // // Verify that our counter has incremented. - // expect(find.text('0'), findsNothing); - // expect(find.text('1'), findsOneWidget); - }); - }); -} diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index 077cea5..8440b51 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -1,7 +1,11 @@ +import 'dart:math'; + import 'package:currency_picker/currency_picker.dart'; +import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:prasule/api/category.dart'; +import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_manager.dart'; @@ -132,7 +136,8 @@ class Wallet { : recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day, ); // add the variable again to check if we aren't missing any entries logger.i( - "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})"); + "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})", + ); } WalletManager.saveWallet(this); // save wallet } @@ -158,6 +163,19 @@ class Wallet { /// Empty wallet used for placeholders static final Wallet empty = Wallet( name: "Empty", + entries: [], + recurringEntries: [], + categories: [ + WalletCategory( + name: "Default", + id: 0, + icon: IconData( + Icons.payments.codePoint, + fontFamily: 'MaterialIcons', + ), + color: Colors.white, + ), + ], currency: Currency.from( json: { "code": "USD", @@ -174,4 +192,66 @@ class Wallet { }, ), ); + + /// Creates test data used for debugging purposes + Future createTestEntries() async { + entries.clear(); + recurringEntries.clear(); + final random = Random(); + for (var i = 0; i < 30; i++) { + entries.add( + WalletSingleEntry( + data: EntryData( + name: "Test Entry #${i + 1}", + amount: random.nextInt(20000).toDouble(), + ), + type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, + date: DateTime( + 2023, + random.nextInt(12) + 1, + random.nextInt(28) + 1, + ), + category: categories[random.nextInt(categories.length)], + id: nextId, + ), + ); + } + + logger.d( + "Created ${entries.length} regular entries", + ); + + for (var i = 0; i < 3; i++) { + final type = random.nextInt(3); + recurringEntries.add( + RecurringWalletEntry( + data: EntryData( + name: "Recurring Entry #${i + 1}", + amount: random.nextInt(20000).toDouble(), + ), + type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income, + date: DateTime( + 2023, + random.nextInt(12) + 1, + random.nextInt(28) + 1, + ), + category: categories[random.nextInt(categories.length)], + id: nextId, + lastRunDate: DateTime.now().subtract( + Duration( + days: (type > 0) ? 3 : 3 * 31, + ), + ), + recurType: (type > 0) ? RecurType.day : RecurType.month, + ), + ); + } + + logger.d( + "Created ${recurringEntries.length} recurring entries", + ); + + // save and reload + await WalletManager.saveWallet(this); + } } diff --git a/lib/api/wallet_manager.dart b/lib/api/wallet_manager.dart index e2c66ab..772c85a 100644 --- a/lib/api/wallet_manager.dart +++ b/lib/api/wallet_manager.dart @@ -24,10 +24,23 @@ class WalletManager { // TODO: do something with unreadable wallets } } - logger.i(wallets.length); return wallets; } + /// Deletes all [Wallet]s + static Future deleteAllData() async { + final path = + Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); + if (!path.existsSync()) { + return; + } + + for (final entry in path.listSync()) { + logger.d("Deleting ${entry.path}"); + entry.deleteSync(); + } + } + /// Loads and returns a single [Wallet] by name static Future loadWallet(String name) async { final path = diff --git a/lib/main.dart b/lib/main.dart index 9a86716..d6791ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,12 +29,15 @@ final logger = Logger(); /// The application itself class MyApp extends StatelessWidget { /// The application itself - const MyApp({super.key}); + const MyApp({super.key, this.locale}); /// If Material You was applied /// /// Used to check if it is supported static bool appliedYou = false; + + /// Override locale, used for testing + final Locale? locale; // This widget is the root of your application. @override Widget build(BuildContext context) { @@ -50,6 +53,7 @@ class MyApp extends StatelessWidget { ...GlobalCupertinoLocalizations.delegates, ], supportedLocales: AppLocalizations.supportedLocales, + locale: locale, title: 'Prašule', theme: ThemeData( colorScheme: _materialYou diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index 2ccd5e5..211ee90 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -522,6 +522,7 @@ class Indicator extends StatelessWidget { /// Text shown next to the indicator circle final String text; + /// Text style of the indicator final TextStyle textStyle; @override diff --git a/lib/views/create_entry.dart b/lib/views/create_entry.dart index 1b27c8c..76320f9 100644 --- a/lib/views/create_entry.dart +++ b/lib/views/create_entry.dart @@ -192,7 +192,9 @@ class _CreateSingleEntryViewState extends State { onPressed: () { if (newEntry.data.name.isEmpty) { showMessage( - AppLocalizations.of(context).errorEmptyName, context); + AppLocalizations.of(context).errorEmptyName, + context, + ); return; } if (widget.editEntry != null) { diff --git a/lib/views/create_recur_entry.dart b/lib/views/create_recur_entry.dart index 1ad51a6..be03897 100644 --- a/lib/views/create_recur_entry.dart +++ b/lib/views/create_recur_entry.dart @@ -315,7 +315,9 @@ class _CreateRecurringEntryViewState extends State { onPressed: () { if (newEntry.data.name.isEmpty) { showMessage( - AppLocalizations.of(context).errorEmptyName, context); + AppLocalizations.of(context).errorEmptyName, + context, + ); return; } if (widget.editEntry != null) { diff --git a/lib/views/home.dart b/lib/views/home.dart index 6b31dac..a7606a4 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,7 +1,6 @@ // ignore_for_file: inference_failure_on_function_invocation import 'dart:async'; -import 'dart:math'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; @@ -16,7 +15,6 @@ import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/entry_data.dart'; -import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_manager.dart'; @@ -88,70 +86,7 @@ class _HomeViewState extends State { onTap: () { // debug option to quickly fill a wallet with data if (selectedWallet == null) return; - selectedWallet!.entries.clear(); - selectedWallet!.recurringEntries.clear(); - final random = Random(); - for (var i = 0; i < 30; i++) { - selectedWallet!.entries.add( - WalletSingleEntry( - data: EntryData( - name: "Test Entry #${i + 1}", - amount: random.nextInt(20000).toDouble(), - ), - type: (random.nextInt(3) > 0) - ? EntryType.expense - : EntryType.income, - date: DateTime( - 2023, - random.nextInt(12) + 1, - random.nextInt(28) + 1, - ), - category: selectedWallet!.categories[ - random.nextInt(selectedWallet!.categories.length)], - id: selectedWallet!.nextId, - ), - ); - } - - logger.d( - "Created ${selectedWallet!.entries.length} regular entries", - ); - - for (var i = 0; i < 3; i++) { - final type = random.nextInt(3); - selectedWallet!.recurringEntries.add( - RecurringWalletEntry( - data: EntryData( - name: "Recurring Entry #${i + 1}", - amount: random.nextInt(20000).toDouble(), - ), - type: (random.nextInt(3) > 0) - ? EntryType.expense - : EntryType.income, - date: DateTime( - 2023, - random.nextInt(12) + 1, - random.nextInt(28) + 1, - ), - category: selectedWallet!.categories[ - random.nextInt(selectedWallet!.categories.length)], - id: selectedWallet!.nextId, - lastRunDate: DateTime.now().subtract( - Duration( - days: (type > 0) ? 3 : 3 * 31, - ), - ), - recurType: (type > 0) ? RecurType.day : RecurType.month, - ), - ); - } - - logger.d( - "Created ${selectedWallet!.recurringEntries.length} recurring entries", - ); - - // save and reload - WalletManager.saveWallet(selectedWallet!).then((value) { + selectedWallet!.createTestEntries().then((_) { Navigator.of(context).pushReplacement( platformRoute( (p0) => const HomeView(), @@ -555,6 +490,7 @@ class _HomeViewState extends State { } description.write("${line.replaceAll(regex, "")}\n"); } + if (!ctx.mounted) return; Navigator.of(ctx).pop(); // show edit final newEntry = diff --git a/lib/views/settings/edit_categories.dart b/lib/views/settings/edit_categories.dart index 832234d..757c87b 100644 --- a/lib/views/settings/edit_categories.dart +++ b/lib/views/settings/edit_categories.dart @@ -146,7 +146,7 @@ class _EditCategoriesViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; + if (!context.mounted) return; await showDialog( context: context, builder: (c) => PlatformDialog( @@ -227,7 +227,7 @@ class _EditCategoriesViewState extends State { await WalletManager.saveWallet( selectedWallet!, ); - if (!mounted) return; + if (!context.mounted) return; Navigator.of(context).pop(); }, child: Text( diff --git a/lib/views/settings/graph_type.dart b/lib/views/settings/graph_type.dart index cc5d57e..2d7c48b 100644 --- a/lib/views/settings/graph_type.dart +++ b/lib/views/settings/graph_type.dart @@ -70,7 +70,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("yearlygraph", 1); _yearly = 1; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -90,7 +90,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("yearlygraph", 2); _yearly = 2; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -128,7 +128,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("monthlygraph", 1); _monthly = 1; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, @@ -148,7 +148,7 @@ class _GraphTypeSettingsViewState extends State { final s = await SharedPreferences.getInstance(); await s.setInt("monthlygraph", 2); _monthly = 2; - if (!mounted) return; + if (!ctx.mounted) return; Navigator.of(ctx).pop(); setState(() {}); }, diff --git a/lib/views/settings/tessdata_list.dart b/lib/views/settings/tessdata_list.dart index a0648a5..855202c 100644 --- a/lib/views/settings/tessdata_list.dart +++ b/lib/views/settings/tessdata_list.dart @@ -75,7 +75,9 @@ class _TessdataListViewState extends State { onPressed: () async { await TessdataApi.deleteData(lang); _tessdata[i][lang] = true; - if (mounted) Navigator.of(context).pop(); + if (context.mounted) { + Navigator.of(context).pop(); + } }, ), PlatformButton( diff --git a/lib/views/setup.dart b/lib/views/setup.dart index e8749e3..cde8424 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -297,7 +297,7 @@ class _SetupViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; + if (!context.mounted) return; await showDialog( context: context, builder: (c) => PlatformDialog( diff --git a/pubspec.lock b/pubspec.lock index e57d14a..759eca0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -325,10 +325,10 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c + sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.3.1" flex_seed_scheme: dependency: transitive description: -- 2.45.2 From 91fc0bb6073f1c7f2180cca5799fb552b6e0ef51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 15:15:10 +0100 Subject: [PATCH 3/9] fix: show current balance on homepage --- lib/api/wallet.dart | 22 +++ lib/l10n/app_cs.arb | 5 +- lib/l10n/app_en.arb | 5 +- lib/util/graphs.dart | 16 +- lib/views/home.dart | 400 ++++++++++++++++++++++++++----------------- 5 files changed, 285 insertions(+), 163 deletions(-) diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index 8440b51..e2dcd1f 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -160,6 +160,28 @@ class Wallet { await WalletManager.saveWallet(this); } + /// Returns the current balance + /// + /// Basically just takes *starterBalance* and adds all the entries to it + double calculateCurrentBalance() { + var toAdd = 0.0; + for (final e in entries) { + toAdd += (e.type == EntryType.income) ? e.data.amount : -e.data.amount; + } + return starterBalance + toAdd; + } + + /// Returns the amount that was made/lost during a month + double calculateMonthStatus(int month, int year) { + var f = 0.0; + for (final e in entries.where( + (element) => element.date.year == year && element.date.month == month, + )) { + f += (e.type == EntryType.income) ? e.data.amount : -e.data.amount; + } + return f; + } + /// Empty wallet used for placeholders static final Wallet empty = Wallet( name: "Empty", diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index d1794ff..4558880 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -87,5 +87,8 @@ "dayCounter":"{count, plural, =1{den} few{dny} many{dnů} other{dnů} }", "yearCounter":"{count, plural, =1{rok} few{rok} many{let} other{let} }", "recurEvery":"{count, plural, =1{Opakovat každý} few{Opakovat každé} many{Opakovat každých} other{Opakovat každých}}", - "startingWithDate": "počínaje datem" + "startingWithDate": "počínaje datem", + "evenMoney":"Váš stav je stejný jako minulý měsíc.", + "balanceStatusA": "Váš stav je ", + "balanceStatusB": " oproti minulému měsíci." } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c2fe305..f6b06cd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -203,5 +203,8 @@ "startingWithDate": "starting", "@startingWithDate":{ "description": "Shown after 'Recur every X Y', e.g. 'Recur every 2 month starting 20th June 2023'" - } + }, + "evenMoney":"You're on the same balance as last month.", + "balanceStatusA": "Your balance is ", + "balanceStatusB": " compared to last month." } \ No newline at end of file diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index 211ee90..1fba13e 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -150,11 +150,11 @@ class ExpensesLineChart extends StatelessWidget { isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), - color: - (MediaQuery.of(context).platformBrightness == Brightness.dark) + color: ((MediaQuery.of(context).platformBrightness == + Brightness.dark) ? Colors.green.shade300 - : Colors.green - .harmonizeWith(Theme.of(context).colorScheme.primary), + : Colors.green) + .harmonizeWith(Theme.of(context).colorScheme.primary), spots: List.generate( yearly ? 12 : date.lastDay, (index) => FlSpot(index.toDouble(), incomeData[index]), @@ -167,11 +167,11 @@ class ExpensesLineChart extends StatelessWidget { isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), - color: - (MediaQuery.of(context).platformBrightness == Brightness.dark) + color: ((MediaQuery.of(context).platformBrightness == + Brightness.dark) ? Colors.red.shade300 - : Colors.red - .harmonizeWith(Theme.of(context).colorScheme.primary), + : Colors.red) + .harmonizeWith(Theme.of(context).colorScheme.primary), spots: List.generate( yearly ? 12 diff --git a/lib/views/home.dart b/lib/views/home.dart index a7606a4..20f4f65 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -222,173 +222,267 @@ class _HomeViewState extends State { ), ], ) - : GroupedListView( - groupHeaderBuilder: (element) => Text( - DateFormat.yMMMM(locale).format(element.date), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - elements: selectedWallet!.entries, - itemComparator: (a, b) => b.date.compareTo(a.date), - groupBy: (e) => DateFormat.yMMMM(locale).format(e.date), - groupComparator: (a, b) { - // TODO: better sorting algorithm lol - final yearA = RegExp(r'\d+').firstMatch(a); - if (yearA == null) return 0; - final yearB = RegExp(r'\d+').firstMatch(b); - if (yearB == null) return 0; - final compareYears = int.parse(yearB.group(0)!) - .compareTo(int.parse(yearA.group(0)!)); - if (compareYears != 0) return compareYears; - final months = List.generate( - 12, - (index) => DateFormat.MMMM(locale).format( - DateTime(2023, index + 1), + : Column( + children: [ + Text( + NumberFormat.compactCurrency( + locale: locale, + symbol: selectedWallet!.currency.symbol, + ).format(selectedWallet!.calculateCurrentBalance()), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, ), - ); - final monthA = RegExp('[^0-9 ]+').firstMatch(a); - if (monthA == null) return 0; - final monthB = RegExp('[^0-9 ]+').firstMatch(b); - if (monthB == null) return 0; - return months.indexOf(monthB.group(0)!).compareTo( - months.indexOf(monthA.group(0)!), - ); - }, - itemBuilder: (context, element) => Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - SlidableAction( - onPressed: (c) { - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (c) => CreateSingleEntryView( - w: selectedWallet!, - editEntry: element, - ), - ), - ) - .then( - (editedEntry) { - if (editedEntry == null) return; - selectedWallet!.entries.remove(element); - selectedWallet!.entries.add(editedEntry); - WalletManager.saveWallet(selectedWallet!); - setState(() {}); - }, - ); - }, - backgroundColor: - Theme.of(context).colorScheme.secondary, - foregroundColor: - Theme.of(context).colorScheme.onSecondary, - icon: Icons.edit, - ), - SlidableAction( - backgroundColor: - Theme.of(context).colorScheme.error, - foregroundColor: - Theme.of(context).colorScheme.onError, - icon: Icons.delete, - onPressed: (c) { - showDialog( - context: context, - builder: (cx) => PlatformDialog( - title: - AppLocalizations.of(context).sureDialog, - content: Text( - AppLocalizations.of(context).deleteSure, - ), - actions: [ - PlatformButton( - text: AppLocalizations.of(context).yes, - onPressed: () { - selectedWallet?.entries.removeWhere( - (e) => e.id == element.id, - ); - - WalletManager.saveWallet( - selectedWallet!, - ); - Navigator.of(cx).pop(); - setState(() {}); - }, - ), - PlatformButton( - text: AppLocalizations.of(context).no, - onPressed: () { - Navigator.of(cx).pop(); - }, - ), - ], - ), - ); - }, - ), - ], ), - child: ListTile( - leading: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: element.category.color, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - element.category.icon, - color: - element.category.color.calculateTextColor(), - ), - ), - ), - title: Text(element.data.name), - subtitle: RichText( + const SizedBox( + height: 5, + ), + if (selectedWallet!.calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ) == + 0) + Text(AppLocalizations.of(context).evenMoney) + else + RichText( text: TextSpan( children: [ TextSpan( - text: NumberFormat.currency( - symbol: selectedWallet!.currency.symbol, - ).format(element.data.amount), + text: AppLocalizations.of(context) + .balanceStatusA, + ), + TextSpan( style: TextStyle( - color: (element.type == EntryType.income) - ? (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.green.shade300 - : Colors.green.harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - ) - : (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.red.shade300 - : Colors.red.harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - ), + color: (selectedWallet! + .calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ) > + 0 + ? ((MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.green.shade300 + : Colors.green) + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ) + : ((MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.red.shade300 + : Colors.red) + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + )), + ), + text: NumberFormat.compactCurrency( + locale: locale, + symbol: selectedWallet!.currency.symbol, + ).format( + selectedWallet!.calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ), ), ), TextSpan( - text: - " | ${DateFormat.MMMd(locale).format(element.date)}", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .background - .calculateTextColor(), - ), + text: AppLocalizations.of(context) + .balanceStatusB, ), ], ), ), + const SizedBox( + height: 10, ), - ), + Expanded( + child: GroupedListView( + groupHeaderBuilder: (element) => Text( + DateFormat.yMMMM(locale).format(element.date), + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + elements: selectedWallet!.entries, + itemComparator: (a, b) => b.date.compareTo(a.date), + groupBy: (e) => + DateFormat.yMMMM(locale).format(e.date), + groupComparator: (a, b) { + // TODO: better sorting algorithm lol + final yearA = RegExp(r'\d+').firstMatch(a); + if (yearA == null) return 0; + final yearB = RegExp(r'\d+').firstMatch(b); + if (yearB == null) return 0; + final compareYears = int.parse(yearB.group(0)!) + .compareTo(int.parse(yearA.group(0)!)); + if (compareYears != 0) return compareYears; + final months = List.generate( + 12, + (index) => DateFormat.MMMM(locale).format( + DateTime(2023, index + 1), + ), + ); + final monthA = RegExp('[^0-9 ]+').firstMatch(a); + if (monthA == null) return 0; + final monthB = RegExp('[^0-9 ]+').firstMatch(b); + if (monthB == null) return 0; + return months.indexOf(monthB.group(0)!).compareTo( + months.indexOf(monthA.group(0)!), + ); + }, + itemBuilder: (context, element) => Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (c) { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (c) => CreateSingleEntryView( + w: selectedWallet!, + editEntry: element, + ), + ), + ) + .then( + (editedEntry) { + if (editedEntry == null) return; + selectedWallet!.entries + .remove(element); + selectedWallet!.entries + .add(editedEntry); + WalletManager.saveWallet( + selectedWallet!, + ); + setState(() {}); + }, + ); + }, + backgroundColor: + Theme.of(context).colorScheme.secondary, + foregroundColor: Theme.of(context) + .colorScheme + .onSecondary, + icon: Icons.edit, + ), + SlidableAction( + backgroundColor: + Theme.of(context).colorScheme.error, + foregroundColor: + Theme.of(context).colorScheme.onError, + icon: Icons.delete, + onPressed: (c) { + showDialog( + context: context, + builder: (cx) => PlatformDialog( + title: AppLocalizations.of(context) + .sureDialog, + content: Text( + AppLocalizations.of(context) + .deleteSure, + ), + actions: [ + PlatformButton( + text: AppLocalizations.of(context) + .yes, + onPressed: () { + selectedWallet?.entries + .removeWhere( + (e) => e.id == element.id, + ); + + WalletManager.saveWallet( + selectedWallet!, + ); + Navigator.of(cx).pop(); + setState(() {}); + }, + ), + PlatformButton( + text: AppLocalizations.of(context) + .no, + onPressed: () { + Navigator.of(cx).pop(); + }, + ), + ], + ), + ); + }, + ), + ], + ), + child: ListTile( + leading: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: element.category.color, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + element.category.icon, + color: element.category.color + .calculateTextColor(), + ), + ), + ), + title: Text(element.data.name), + subtitle: RichText( + text: TextSpan( + children: [ + TextSpan( + text: NumberFormat.currency( + symbol: + selectedWallet!.currency.symbol, + ).format(element.data.amount), + style: TextStyle( + color: (element.type == + EntryType.income) + ? (MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.green.shade300 + : Colors.green.harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ) + : (MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.red.shade300 + : Colors.red.harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + TextSpan( + text: + " | ${DateFormat.MMMd(locale).format(element.date)}", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .background + .calculateTextColor(), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], ), ), ), -- 2.45.2 From 805fc2bf1771b207462c52f49e0854affdd0ab8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 15:29:28 +0100 Subject: [PATCH 4/9] docs: add badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1476323..cef9b77 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # prasule -[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) +[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Commit Style: Conventional Commits](https://img.shields.io/badge/commit%20style-conventional%20commits-pink)](https://www.conventionalcommits.org/en/v1.0.0/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) Expense manager -- 2.45.2 From 91be906894b01d8e1956f69edd156c37fc676db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 19:20:41 +0100 Subject: [PATCH 5/9] feat: allow searching through entries --- CHANGELOG.md | 4 + android/app/src/main/AndroidManifest.xml | 3 +- lib/l10n/app_cs.arb | 3 +- lib/l10n/app_en.arb | 3 +- lib/pw/platformfield.dart | 8 +- lib/views/home.dart | 876 +++++++++++++---------- pubspec.yaml | 2 +- 7 files changed, 507 insertions(+), 392 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0114d..3d1b1ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ + +# 1.0.0-alpha+5 +- Add tests +- Add searching through entries to homepage # 1.0.0-alpha+4 - Fix OCR downloads # 1.0.0-alpha+3 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 23e2c46..91d67ed 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,8 @@ + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> { this.textStyle, this.textAlign = TextAlign.start, this.maxLines = 1, + this.focusNode, + this.inputBorder = const OutlineInputBorder(), }); final TextEditingController? controller; final bool? enabled; @@ -34,6 +36,8 @@ class PlatformField extends PlatformWidget { final TextStyle? textStyle; final TextAlign textAlign; final int? maxLines; + final InputBorder inputBorder; + final FocusNode? focusNode; @override TextField createAndroidWidget(BuildContext context) => TextField( @@ -43,10 +47,11 @@ class PlatformField extends PlatformWidget { obscureText: obscureText, decoration: InputDecoration( labelText: labelText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), + border: inputBorder, ), autocorrect: autocorrect, keyboardType: keyboardType, + focusNode: focusNode, style: textStyle, inputFormatters: inputFormatters, onChanged: onChanged, @@ -66,6 +71,7 @@ class PlatformField extends PlatformWidget { keyboardType: keyboardType, inputFormatters: inputFormatters, onChanged: onChanged, + focusNode: focusNode, maxLines: maxLines, style: textStyle, ); diff --git a/lib/views/home.dart b/lib/views/home.dart index be5637a..7223ccc 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -22,6 +22,7 @@ import 'package:prasule/main.dart'; import 'package:prasule/network/tessdata.dart'; import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; +import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/drawer.dart'; import 'package:prasule/util/text_color.dart'; @@ -44,6 +45,9 @@ class _HomeViewState extends State { List wallets = []; DateTime? prevDate; late String locale; + var _searchActive = false; + var _filter = ""; + final searchFocus = FocusNode(); @override void didChangeDependencies() { super.didChangeDependencies(); @@ -73,417 +77,515 @@ class _HomeViewState extends State { @override Widget build(BuildContext context) { - return Scaffold( - drawer: makeDrawer(context, 1), - floatingActionButton: SpeedDial( - icon: Icons.add, - activeIcon: Icons.close, - children: [ - if (kDebugMode) + return PopScope( + canPop: !_searchActive, // don't pop when we just want + // to deactivate searchfield + onPopInvoked: (b) { + if (b) return; + _searchActive = false; + _filter = ""; + setState(() {}); + }, + child: Scaffold( + drawer: makeDrawer(context, 1), + floatingActionButton: SpeedDial( + icon: Icons.add, + activeIcon: Icons.close, + children: [ + if (kDebugMode) + SpeedDialChild( + child: const Icon(Icons.bug_report), + label: AppLocalizations.of(context).createTestData, + onTap: () { + // debug option to quickly fill a wallet with data + if (selectedWallet == null) return; + selectedWallet!.createTestEntries().then((_) { + Navigator.of(context).pushReplacement( + platformRoute( + (p0) => const HomeView(), + ), + ); + }); + }, + ), SpeedDialChild( - child: const Icon(Icons.bug_report), - label: AppLocalizations.of(context).createTestData, - onTap: () { - // debug option to quickly fill a wallet with data - if (selectedWallet == null) return; - selectedWallet!.createTestEntries().then((_) { - Navigator.of(context).pushReplacement( - platformRoute( - (p0) => const HomeView(), - ), - ); - }); + child: const Icon(Icons.edit), + label: AppLocalizations.of(context).addNew, + onTap: () async { + final sw = await Navigator.of(context).push( + MaterialPageRoute( + builder: (c) => CreateSingleEntryView(w: selectedWallet!), + ), + ); + if (sw != null) { + selectedWallet = sw; + } + setState(() {}); }, ), - SpeedDialChild( - child: const Icon(Icons.edit), - label: AppLocalizations.of(context).addNew, - onTap: () async { - final sw = await Navigator.of(context).push( - MaterialPageRoute( - builder: (c) => CreateSingleEntryView(w: selectedWallet!), - ), - ); - if (sw != null) { - selectedWallet = sw; - } - setState(() {}); - }, - ), - SpeedDialChild( - child: const Icon(Icons.camera_alt), - label: AppLocalizations.of(context).addCamera, - onTap: () async { - final picker = ImagePicker(); - final media = await picker.pickImage(source: ImageSource.camera); - logger.i(media?.name); - }, - ), - SpeedDialChild( - child: const Icon(Icons.image), - label: AppLocalizations.of(context).addGallery, - onTap: () { - startOcr(ImageSource.gallery); - }, - ), - ], - ), - appBar: AppBar( - title: DropdownButton( - value: - (selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!), - items: [ - ...wallets.map( - (e) => DropdownMenuItem( - value: wallets.indexOf( - e, - ), - child: Text(e.name), - ), + SpeedDialChild( + child: const Icon(Icons.camera_alt), + label: AppLocalizations.of(context).addCamera, + onTap: () async { + final picker = ImagePicker(); + final media = + await picker.pickImage(source: ImageSource.camera); + logger.i(media?.name); + }, ), - DropdownMenuItem( - value: -1, - child: Text(AppLocalizations.of(context).newWallet), + SpeedDialChild( + child: const Icon(Icons.image), + label: AppLocalizations.of(context).addGallery, + onTap: () { + startOcr(ImageSource.gallery); + }, ), ], - onChanged: (v) async { - if (v == null || v == -1) { - await Navigator.of(context).push( - platformRoute( - (c) => const SetupView( - newWallet: true, + ), + appBar: AppBar( + title: AnimatedCrossFade( + duration: const Duration(milliseconds: 500), + crossFadeState: _searchActive + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: PlatformField( + inputBorder: InputBorder.none, + focusNode: searchFocus, + onChanged: (e) { + _filter = e; + setState(() {}); + }, + labelText: AppLocalizations.of(context).searchLabel, + ), + secondChild: DropdownButton( + value: (selectedWallet == null) + ? -1 + : wallets.indexOf(selectedWallet!), + items: [ + ...wallets.map( + (e) => DropdownMenuItem( + value: wallets.indexOf( + e, + ), + child: Text(e.name), ), ), - ); - wallets = await WalletManager.listWallets(); - selectedWallet = wallets.last; - setState(() {}); - return; - } - selectedWallet = wallets[v]; - setState(() {}); - }, - ), - actions: [ - PopupMenuButton( - itemBuilder: (context) => [ - AppLocalizations.of(context).settings, - AppLocalizations.of(context).about, - ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), - onSelected: (value) { - if (value == AppLocalizations.of(context).settings) { - Navigator.of(context) - .push( - platformRoute( - (context) => const SettingsView(), - ), - ) - .then((value) async { - selectedWallet = - await WalletManager.loadWallet(selectedWallet!.name); - }); - } else if (value == AppLocalizations.of(context).about) { - showAboutDialog( - context: context, - applicationLegalese: AppLocalizations.of(context).license, - applicationName: "Prašule", - ); - } - }, - ), - ], - ), - body: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height, - child: (selectedWallet == null) - ? const Column( - children: [ - SizedBox( - width: 40, - height: 40, - child: CircularProgressIndicator(), + DropdownMenuItem( + value: -1, + child: Text(AppLocalizations.of(context).newWallet), + ), + ], + onChanged: (v) async { + if (v == null || v == -1) { + await Navigator.of(context).push( + platformRoute( + (c) => const SetupView( + newWallet: true, + ), ), - ], - ) - : (selectedWallet!.entries.isEmpty) - ? Column( - children: [ - Text( - AppLocalizations.of(context).noEntries, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - Text( - AppLocalizations.of(context).noEntriesSub, - ), - ], + ); + wallets = await WalletManager.listWallets(); + selectedWallet = wallets.last; + setState(() {}); + return; + } + selectedWallet = wallets[v]; + setState(() {}); + }, + ), + ), + actions: [ + if (!_searchActive) + IconButton( + onPressed: () { + _searchActive = true; + setState(() {}); + }, + icon: const Icon(Icons.search), + ), + if (!_searchActive) + PopupMenuButton( + itemBuilder: (context) => [ + AppLocalizations.of(context).settings, + AppLocalizations.of(context).about, + ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), + onSelected: (value) { + if (value == AppLocalizations.of(context).settings) { + Navigator.of(context) + .push( + platformRoute( + (context) => const SettingsView(), + ), ) - : Column( - children: [ - Text( - NumberFormat.compactCurrency( - locale: locale, - symbol: selectedWallet!.currency.symbol, - ).format(selectedWallet!.calculateCurrentBalance()), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 22, + .then((value) async { + selectedWallet = + await WalletManager.loadWallet(selectedWallet!.name); + }); + } else if (value == AppLocalizations.of(context).about) { + showAboutDialog( + context: context, + applicationLegalese: AppLocalizations.of(context).license, + applicationName: "Prašule", + ); + } + }, + ), + ], + ), + body: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height, + child: (selectedWallet == null) + ? const Column( + children: [ + SizedBox( + width: 40, + height: 40, + child: CircularProgressIndicator(), + ), + ], + ) + : (selectedWallet!.entries.isEmpty) + ? Column( + children: [ + Text( + AppLocalizations.of(context).noEntries, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox( - height: 5, - ), - if (selectedWallet!.calculateMonthStatus( - DateTime.now().month, - DateTime.now().year, - ) == - 0) - Text(AppLocalizations.of(context).evenMoney) - else - RichText( - text: TextSpan( + Text( + AppLocalizations.of(context).noEntriesSub, + ), + ], + ) + : Overlay( + initialEntries: [ + OverlayEntry( + builder: (context) => Column( children: [ - TextSpan( - text: AppLocalizations.of(context) - .balanceStatusA, - ), - TextSpan( - style: TextStyle( - color: (selectedWallet! - .calculateMonthStatus( - DateTime.now().month, - DateTime.now().year, - ) > - 0 - ? ((MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.green.shade300 - : Colors.green) - .harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - ) - : ((MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.red.shade300 - : Colors.red) - .harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - )), - ), - text: NumberFormat.compactCurrency( + Text( + NumberFormat.compactCurrency( locale: locale, symbol: selectedWallet!.currency.symbol, ).format( - selectedWallet!.calculateMonthStatus( - DateTime.now().month, - DateTime.now().year, - ), + selectedWallet!.calculateCurrentBalance(), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, ), ), - TextSpan( - text: AppLocalizations.of(context) - .balanceStatusB, + const SizedBox( + height: 5, + ), + if (selectedWallet!.calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ) == + 0) + Text(AppLocalizations.of(context).evenMoney) + else + RichText( + text: TextSpan( + children: [ + TextSpan( + text: AppLocalizations.of(context) + .balanceStatusA, + ), + TextSpan( + style: TextStyle( + color: (selectedWallet! + .calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ) > + 0 + ? ((MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.green.shade300 + : Colors.green) + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ) + : ((MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.red.shade300 + : Colors.red) + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + )), + ), + text: NumberFormat.compactCurrency( + locale: locale, + symbol: + selectedWallet!.currency.symbol, + ).format( + selectedWallet! + .calculateMonthStatus( + DateTime.now().month, + DateTime.now().year, + ), + ), + ), + TextSpan( + text: AppLocalizations.of(context) + .balanceStatusB, + ), + ], + ), + ), + const SizedBox( + height: 10, + ), + Expanded( + child: GroupedListView( + groupHeaderBuilder: (element) => Text( + DateFormat.yMMMM(locale) + .format(element.date), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + elements: selectedWallet!.entries + .where((element) => + element.data.name.contains(_filter)) + .toList(), + itemComparator: (a, b) => + b.date.compareTo(a.date), + groupBy: (e) => + DateFormat.yMMMM(locale).format(e.date), + groupComparator: (a, b) { + // TODO: better sorting algorithm lol + final yearA = + RegExp(r'\d+').firstMatch(a); + if (yearA == null) return 0; + final yearB = + RegExp(r'\d+').firstMatch(b); + if (yearB == null) return 0; + final compareYears = + int.parse(yearB.group(0)!).compareTo( + int.parse(yearA.group(0)!), + ); + if (compareYears != 0) { + return compareYears; + } + final months = List.generate( + 12, + (index) => + DateFormat.MMMM(locale).format( + DateTime(2023, index + 1), + ), + ); + final monthA = + RegExp('[^0-9 ]+').firstMatch(a); + if (monthA == null) return 0; + final monthB = + RegExp('[^0-9 ]+').firstMatch(b); + if (monthB == null) return 0; + return months + .indexOf(monthB.group(0)!) + .compareTo( + months.indexOf(monthA.group(0)!), + ); + }, + itemBuilder: (context, element) => Slidable( + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (c) { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (c) => + CreateSingleEntryView( + w: selectedWallet!, + editEntry: element, + ), + ), + ) + .then( + (editedEntry) { + if (editedEntry == null) { + return; + } + selectedWallet!.entries + .remove(element); + selectedWallet!.entries + .add(editedEntry); + WalletManager.saveWallet( + selectedWallet!, + ); + setState(() {}); + }, + ); + }, + backgroundColor: Theme.of(context) + .colorScheme + .secondary, + foregroundColor: Theme.of(context) + .colorScheme + .onSecondary, + icon: Icons.edit, + ), + SlidableAction( + backgroundColor: Theme.of(context) + .colorScheme + .error, + foregroundColor: Theme.of(context) + .colorScheme + .onError, + icon: Icons.delete, + onPressed: (c) { + showDialog( + context: context, + builder: (cx) => PlatformDialog( + title: AppLocalizations.of( + context, + ).sureDialog, + content: Text( + AppLocalizations.of(context) + .deleteSure, + ), + actions: [ + PlatformButton( + text: AppLocalizations.of( + context, + ).yes, + onPressed: () { + selectedWallet?.entries + .removeWhere( + (e) => + e.id == + element.id, + ); + + WalletManager + .saveWallet( + selectedWallet!, + ); + Navigator.of(cx).pop(); + setState(() {}); + }, + ), + PlatformButton( + text: AppLocalizations.of( + context, + ).no, + onPressed: () { + Navigator.of(cx).pop(); + }, + ), + ], + ), + ); + }, + ), + ], + ), + child: ListTile( + leading: Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(16), + color: element.category.color, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + element.category.icon, + color: element.category.color + .calculateTextColor(), + ), + ), + ), + title: Text(element.data.name), + subtitle: RichText( + text: TextSpan( + children: [ + TextSpan( + text: NumberFormat.currency( + symbol: selectedWallet! + .currency.symbol, + ).format(element.data.amount), + style: TextStyle( + color: (element.type == + EntryType.income) + ? (MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors + .green.shade300 + : Colors.green + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ) + : (MediaQuery.of(context) + .platformBrightness == + Brightness.dark) + ? Colors.red.shade300 + : Colors.red + .harmonizeWith( + Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + TextSpan( + text: + " | ${DateFormat.MMMd(locale).format(element.date)}", + style: TextStyle( + color: Theme.of(context) + .colorScheme + .background + .calculateTextColor(), + ), + ), + ], + ), + ), + ), + ), + ), ), ], ), ), - const SizedBox( - height: 10, - ), - Expanded( - child: GroupedListView( - groupHeaderBuilder: (element) => Text( - DateFormat.yMMMM(locale).format(element.date), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ), - elements: selectedWallet!.entries, - itemComparator: (a, b) => b.date.compareTo(a.date), - groupBy: (e) => - DateFormat.yMMMM(locale).format(e.date), - groupComparator: (a, b) { - // TODO: better sorting algorithm lol - final yearA = RegExp(r'\d+').firstMatch(a); - if (yearA == null) return 0; - final yearB = RegExp(r'\d+').firstMatch(b); - if (yearB == null) return 0; - final compareYears = int.parse(yearB.group(0)!) - .compareTo(int.parse(yearA.group(0)!)); - if (compareYears != 0) return compareYears; - final months = List.generate( - 12, - (index) => DateFormat.MMMM(locale).format( - DateTime(2023, index + 1), - ), - ); - final monthA = RegExp('[^0-9 ]+').firstMatch(a); - if (monthA == null) return 0; - final monthB = RegExp('[^0-9 ]+').firstMatch(b); - if (monthB == null) return 0; - return months.indexOf(monthB.group(0)!).compareTo( - months.indexOf(monthA.group(0)!), - ); - }, - itemBuilder: (context, element) => Slidable( - endActionPane: ActionPane( - motion: const ScrollMotion(), - children: [ - SlidableAction( - onPressed: (c) { - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (c) => CreateSingleEntryView( - w: selectedWallet!, - editEntry: element, - ), - ), - ) - .then( - (editedEntry) { - if (editedEntry == null) return; - selectedWallet!.entries - .remove(element); - selectedWallet!.entries - .add(editedEntry); - WalletManager.saveWallet( - selectedWallet!, - ); - setState(() {}); - }, - ); - }, - backgroundColor: - Theme.of(context).colorScheme.secondary, - foregroundColor: Theme.of(context) - .colorScheme - .onSecondary, - icon: Icons.edit, - ), - SlidableAction( - backgroundColor: - Theme.of(context).colorScheme.error, - foregroundColor: - Theme.of(context).colorScheme.onError, - icon: Icons.delete, - onPressed: (c) { - showDialog( - context: context, - builder: (cx) => PlatformDialog( - title: AppLocalizations.of(context) - .sureDialog, - content: Text( - AppLocalizations.of(context) - .deleteSure, - ), - actions: [ - PlatformButton( - text: AppLocalizations.of(context) - .yes, - onPressed: () { - selectedWallet?.entries - .removeWhere( - (e) => e.id == element.id, - ); - - WalletManager.saveWallet( - selectedWallet!, - ); - Navigator.of(cx).pop(); - setState(() {}); - }, - ), - PlatformButton( - text: AppLocalizations.of(context) - .no, - onPressed: () { - Navigator.of(cx).pop(); - }, - ), - ], - ), - ); - }, - ), - ], - ), - child: ListTile( - leading: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: element.category.color, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - element.category.icon, - color: element.category.color - .calculateTextColor(), - ), - ), - ), - title: Text(element.data.name), - subtitle: RichText( - text: TextSpan( - children: [ - TextSpan( - text: NumberFormat.currency( - symbol: - selectedWallet!.currency.symbol, - ).format(element.data.amount), - style: TextStyle( - color: (element.type == - EntryType.income) - ? (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.green.shade300 - : Colors.green.harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - ) - : (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Colors.red.shade300 - : Colors.red.harmonizeWith( - Theme.of(context) - .colorScheme - .primary, - ), - ), - ), - TextSpan( - text: - " | ${DateFormat.MMMd(locale).format(element.date)}", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .background - .calculateTextColor(), - ), - ), - ], - ), - ), + OverlayEntry( + builder: (context) => SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: GestureDetector( + onTap: () { + if (!_searchActive) return; + if (!searchFocus.hasFocus) { + _searchActive = false; + _filter = ""; + setState(() {}); + return; + } + searchFocus.unfocus(); + }, ), ), ), - ), - ], - ), + ], + ), + ), ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index cd1ca24..8f456f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: prasule description: Open-source private expense tracker -version: 1.0.0-alpha+4 +version: 1.0.0-alpha+5 environment: sdk: '>=3.1.0-262.2.beta <4.0.0' -- 2.45.2 From 7fed91e8112ec2b407892dbf3fcd822134a09fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 20:26:10 +0100 Subject: [PATCH 6/9] fix: replace platformdialog with adaptive dialog --- CHANGELOG.md | 1 + lib/pw/platformdialog.dart | 31 ----------- lib/views/home.dart | 32 ++++++----- lib/views/recurring_view.dart | 8 +-- lib/views/settings/edit_categories.dart | 17 +++--- lib/views/settings/graph_type.dart | 11 ++-- lib/views/settings/tessdata_list.dart | 16 +++--- lib/views/setup.dart | 74 ++++++++++++++----------- 8 files changed, 85 insertions(+), 105 deletions(-) delete mode 100644 lib/pw/platformdialog.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d1b1ff..3b77505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 1.0.0-alpha+5 - Add tests - Add searching through entries to homepage +- Replace `PlatformDialog` with `AlertDialog.adaptive` # 1.0.0-alpha+4 - Fix OCR downloads # 1.0.0-alpha+3 diff --git a/lib/pw/platformdialog.dart b/lib/pw/platformdialog.dart deleted file mode 100644 index 070d6f6..0000000 --- a/lib/pw/platformdialog.dart +++ /dev/null @@ -1,31 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:prasule/pw/platformwidget.dart'; - -/// A [PlatformWidget] implementation of a dialog -class PlatformDialog extends PlatformWidget { - const PlatformDialog( - {required this.title, super.key, this.content, this.actions = const [],}); - final String title; - final Widget? content; - final List actions; - - @override - AlertDialog createAndroidWidget(BuildContext context) => AlertDialog( - title: Text(title), - content: - (content != null) ? SingleChildScrollView(child: content) : null, - actions: actions, - ); - - @override - CupertinoAlertDialog createIosWidget(BuildContext context) => - CupertinoAlertDialog( - title: Text(title), - content: - (content != null) ? SingleChildScrollView(child: content) : null, - actions: actions, - ); -} diff --git a/lib/views/home.dart b/lib/views/home.dart index 7223ccc..d5a13fc 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -21,7 +21,6 @@ import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/main.dart'; import 'package:prasule/network/tessdata.dart'; import 'package:prasule/pw/platformbutton.dart'; -import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/drawer.dart'; @@ -448,12 +447,15 @@ class _HomeViewState extends State { .onError, icon: Icons.delete, onPressed: (c) { - showDialog( + showAdaptiveDialog( context: context, - builder: (cx) => PlatformDialog( - title: AppLocalizations.of( - context, - ).sureDialog, + builder: (cx) => + AlertDialog.adaptive( + title: Text( + AppLocalizations.of( + context, + ).sureDialog, + ), content: Text( AppLocalizations.of(context) .deleteSure, @@ -595,10 +597,10 @@ class _HomeViewState extends State { final availableLanguages = await TessdataApi.getDownloadedData(); if (availableLanguages.isEmpty) { if (!mounted) return; - await showDialog( + await showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( - title: AppLocalizations.of(context).missingOcr, + builder: (c) => AlertDialog.adaptive( + title: Text(AppLocalizations.of(context).missingOcr), actions: [ PlatformButton( text: AppLocalizations.of(context).download, @@ -628,10 +630,10 @@ class _HomeViewState extends State { List.filled(availableLanguages.length, false); selectedLanguages[0] = true; - await showDialog( + await showAdaptiveDialog( context: context, builder: (c) => StatefulBuilder( - builder: (ctx, setState) => PlatformDialog( + builder: (ctx, setState) => AlertDialog.adaptive( actions: [ TextButton( onPressed: () async { @@ -652,10 +654,10 @@ class _HomeViewState extends State { logger.i(selected); if (!mounted) return; unawaited( - showDialog( + showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( - title: AppLocalizations.of(context).ocrLoading, + builder: (c) => AlertDialog.adaptive( + title: Text(AppLocalizations.of(context).ocrLoading), ), barrierDismissible: false, ), @@ -723,7 +725,7 @@ class _HomeViewState extends State { child: const Text("Cancel"), ), ], - title: AppLocalizations.of(context).ocrSelect, + title: Text(AppLocalizations.of(context).ocrSelect), content: Column( children: [ ...List.generate( diff --git a/lib/views/recurring_view.dart b/lib/views/recurring_view.dart index 49bc3e3..ca1797d 100644 --- a/lib/views/recurring_view.dart +++ b/lib/views/recurring_view.dart @@ -11,7 +11,6 @@ import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/pw/platformbutton.dart'; -import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/drawer.dart'; import 'package:prasule/util/text_color.dart'; @@ -215,9 +214,10 @@ class _RecurringEntriesViewState extends State { onPressed: (c) { showDialog( context: context, - builder: (cx) => PlatformDialog( - title: - AppLocalizations.of(context).sureDialog, + builder: (cx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).sureDialog, + ), content: Text( AppLocalizations.of(context).deleteSure, ), diff --git a/lib/views/settings/edit_categories.dart b/lib/views/settings/edit_categories.dart index 757c87b..22c5a94 100644 --- a/lib/views/settings/edit_categories.dart +++ b/lib/views/settings/edit_categories.dart @@ -12,7 +12,6 @@ import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/main.dart'; import 'package:prasule/pw/platformbutton.dart'; -import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/text_color.dart'; @@ -147,9 +146,9 @@ class _EditCategoriesViewState extends State { .getBool("useMaterialYou") ?? false; if (!context.mounted) return; - await showDialog( + await showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( + builder: (c) => AlertDialog.adaptive( actions: [ PlatformButton( text: AppLocalizations.of(context).done, @@ -158,8 +157,8 @@ class _EditCategoriesViewState extends State { }, ), ], - title: - AppLocalizations.of(context).pickColor, + title: Text( + AppLocalizations.of(context).pickColor), content: Column( children: [ ColorPicker( @@ -215,9 +214,9 @@ class _EditCategoriesViewState extends State { final controller = TextEditingController( text: selectedWallet!.categories[i].name, ); - showDialog( + showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( + builder: (c) => AlertDialog.adaptive( actions: [ TextButton( onPressed: () async { @@ -243,8 +242,8 @@ class _EditCategoriesViewState extends State { ), ), ], - title: AppLocalizations.of(context) - .setupCategoriesEditingName, + title: Text(AppLocalizations.of(context) + .setupCategoriesEditingName), content: SizedBox( width: 400, child: diff --git a/lib/views/settings/graph_type.dart b/lib/views/settings/graph_type.dart index 2d7c48b..404f055 100644 --- a/lib/views/settings/graph_type.dart +++ b/lib/views/settings/graph_type.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:prasule/pw/platformdialog.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -50,10 +49,10 @@ class _GraphTypeSettingsViewState extends State { ? AppLocalizations.of(context).barChart : AppLocalizations.of(context).lineChart, ), - onPressed: (c) => showDialog( + onPressed: (c) => showAdaptiveDialog( context: c, - builder: (ctx) => PlatformDialog( - title: AppLocalizations.of(context).selectType, + builder: (ctx) => AlertDialog.adaptive( + title: Text(AppLocalizations.of(context).selectType), content: Column( children: [ SizedBox( @@ -110,8 +109,8 @@ class _GraphTypeSettingsViewState extends State { ), onPressed: (c) => showDialog( context: c, - builder: (ctx) => PlatformDialog( - title: AppLocalizations.of(context).selectType, + builder: (ctx) => AlertDialog.adaptive( + title: Text(AppLocalizations.of(context).selectType), content: Column( children: [ SizedBox( diff --git a/lib/views/settings/tessdata_list.dart b/lib/views/settings/tessdata_list.dart index 855202c..99382d3 100644 --- a/lib/views/settings/tessdata_list.dart +++ b/lib/views/settings/tessdata_list.dart @@ -9,7 +9,6 @@ import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:prasule/main.dart'; import 'package:prasule/network/tessdata.dart'; import 'package:prasule/pw/platformbutton.dart'; -import 'package:prasule/pw/platformdialog.dart'; /// Used to manage downloaded Tessdata for OCR class TessdataListView extends StatefulWidget { @@ -62,10 +61,11 @@ class _TessdataListViewState extends State { final lang = _tessdata[i].keys.first; if (_tessdata[i][lang]!) { // deleting data - await showDialog( + await showAdaptiveDialog( context: context, - builder: (context) => PlatformDialog( - title: AppLocalizations.of(context).sureDialog, + builder: (context) => AlertDialog.adaptive( + title: + Text(AppLocalizations.of(context).sureDialog), content: Text( AppLocalizations.of(context).deleteOcr(lang), ), @@ -98,11 +98,11 @@ class _TessdataListViewState extends State { final progressStream = StreamController(); unawaited( - showDialog( + showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( - title: AppLocalizations.of(context) - .langDownloadDialog(lang), + builder: (c) => AlertDialog.adaptive( + title: Text(AppLocalizations.of(context) + .langDownloadDialog(lang)), content: StreamBuilder( builder: (context, snapshot) { if (snapshot.connectionState == diff --git a/lib/views/setup.dart b/lib/views/setup.dart index cde8424..f090ad3 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -1,5 +1,7 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'dart:async'; + import 'package:currency_picker/currency_picker.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; @@ -12,7 +14,6 @@ import 'package:prasule/api/category.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/pw/platformbutton.dart'; -import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/show_message.dart'; @@ -118,11 +119,22 @@ class _SetupViewState extends State { next: Text(AppLocalizations.of(context).next), back: Text(AppLocalizations.of(context).back), done: Text(AppLocalizations.of(context).finish), - onDone: () { + onDone: () async { if (name.isEmpty) { - showMessage( - AppLocalizations.of(context).errorEmptyName, - context, + unawaited( + showMessage( + AppLocalizations.of(context).errorEmptyName, + context, + ), + ); + return; + } + if (await WalletManager.exists(name) && context.mounted) { + unawaited( + showMessage( + AppLocalizations.of(context).walletExists, + context, + ), ); return; } @@ -131,25 +143,19 @@ class _SetupViewState extends State { currency: _selectedCurrency, categories: categories, ); - WalletManager.saveWallet(wallet).then( - (value) { - if (!value) { - showMessage( - AppLocalizations.of(context).walletExists, - context, - ); - return; - } - if (widget.newWallet) { - Navigator.of(context).pop(); - return; - } - Navigator.of(context).pushReplacement( - platformRoute( - (c) => const HomeView(), - ), - ); - }, + await WalletManager.saveWallet(wallet); + + if (widget.newWallet && context.mounted) { + Navigator.of(context).pop(); + return; + } + if (!context.mounted) return; + unawaited( + Navigator.of(context).pushReplacement( + platformRoute( + (c) => const HomeView(), + ), + ), ); }, pages: [ @@ -298,9 +304,9 @@ class _SetupViewState extends State { .getBool("useMaterialYou") ?? false; if (!context.mounted) return; - await showDialog( + await showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( + builder: (c) => AlertDialog.adaptive( actions: [ PlatformButton( text: AppLocalizations.of(context) @@ -310,8 +316,10 @@ class _SetupViewState extends State { }, ), ], - title: AppLocalizations.of(context) - .pickColor, + title: Text( + AppLocalizations.of(context) + .pickColor, + ), content: Column( children: [ ColorPicker( @@ -363,9 +371,9 @@ class _SetupViewState extends State { final controller = TextEditingController( text: categories[i].name, ); - showDialog( + showAdaptiveDialog( context: context, - builder: (c) => PlatformDialog( + builder: (c) => AlertDialog.adaptive( actions: [ TextButton( onPressed: () { @@ -390,8 +398,10 @@ class _SetupViewState extends State { ), ), ], - title: AppLocalizations.of(context) - .setupCategoriesEditingName, + title: Text( + AppLocalizations.of(context) + .setupCategoriesEditingName, + ), content: SizedBox( width: 400, child: PlatformField( -- 2.45.2 From 6500c7e5e87a8bb5779833be0fbbe6f8e9fa4288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 23:32:21 +0100 Subject: [PATCH 7/9] feat: import/export data closes #25 --- lib/api/wallet_manager.dart | 131 +++++++++++++++++++++ lib/l10n/app_cs.arb | 15 ++- lib/l10n/app_en.arb | 15 ++- lib/views/home.dart | 43 ++----- lib/views/settings/settings.dart | 188 +++++++++++++++++++++++++++++++ pubspec.lock | 130 ++------------------- pubspec.yaml | 3 +- 7 files changed, 370 insertions(+), 155 deletions(-) diff --git a/lib/api/wallet_manager.dart b/lib/api/wallet_manager.dart index 772c85a..2f07729 100644 --- a/lib/api/wallet_manager.dart +++ b/lib/api/wallet_manager.dart @@ -1,6 +1,10 @@ import 'dart:convert'; import 'dart:io'; +import 'package:archive/archive_io.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; +import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/main.dart'; @@ -41,6 +45,126 @@ class WalletManager { } } + /// Creates a ZIP archive from all wallets + static Future exportAllWallets() async { + if (kIsWeb) { + // TODO + return; + } + + final archive = Archive(); + for (final w in Directory( + "${(await getApplicationDocumentsDirectory()).path}/wallets", + ).listSync()) { + if (w is! File) continue; + logger.i("Zipping ${w.path.split("/").last}"); + final wf = w; + archive.addFile( + ArchiveFile.stream( + wf.path.split("/").last, + wf.lengthSync(), + InputFileStream(wf.path), + ), + ); + } + if (!await FlutterFileDialog.isPickDirectorySupported()) { + File( + "${(await getApplicationDocumentsDirectory()).path}/export_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.zip", + ).writeAsBytesSync(ZipEncoder().encode(archive) ?? []); + return; + } + final dir = await FlutterFileDialog.pickDirectory(); + if (dir == null) return; + await FlutterFileDialog.saveFileToDirectory( + directory: dir, + data: Uint8List.fromList(ZipEncoder().encode(archive) ?? []), + fileName: "export_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.zip", + mimeType: "application/zip", + ); + } + + /// Exports a single [Wallet] + static Future exportWallet({Wallet? wallet, String? name}) async { + if (wallet == null && name == null) { + throw Exception("You need to specify either a wallet or a name"); + } + final n = name ?? wallet!.name; + + if (!await FlutterFileDialog.isPickDirectorySupported()) { + File("${(await getApplicationDocumentsDirectory()).path}/wallets/$n") + .copySync( + "${await getApplicationDocumentsDirectory()}/export_${n.replaceAll(RegExp('[|\\?*<":>+\[\]/\' ]+'), '_')}_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.json", + ); + return; + } + final dir = await FlutterFileDialog.pickDirectory(); + if (dir == null) return; + await FlutterFileDialog.saveFileToDirectory( + directory: dir, + data: + File("${(await getApplicationDocumentsDirectory()).path}/wallets/$n") + .readAsBytesSync(), + fileName: + "export_${n.replaceAll(RegExp('[|\\?*<":>+\[\]/\' ]+'), '_')}_${DateFormat("dd_MM_yyyy").format(DateTime.now())}.json", + mimeType: "application/json", + ); + } + + /// Import a single wallet + static Future importWallet({String? data}) async { + var d = data ?? ""; + if (data == null) { + final filePath = await FlutterFileDialog.pickFile( + params: const OpenFileDialogParams( + mimeTypesFilter: ["application/json"], + fileExtensionsFilter: ["json"], + ), + ); + if (filePath == null) return; + d = File(filePath).readAsStringSync(); + } + final w = Wallet.fromJson(jsonDecode(d) as Map); + if (await WalletManager.exists(w.name)) { + throw Exception("Wallet already exists!"); + } + await WalletManager.saveWallet( + w, + ); + } + + /// Imports wallets from a ZIP archive + static Future importArchive() async { + final filePath = await FlutterFileDialog.pickFile( + params: const OpenFileDialogParams( + mimeTypesFilter: ["application/zip"], + fileExtensionsFilter: ["zip"], + ), + ); + if (filePath == null) return; + if (kIsWeb) { + // TODO + return; + } + final temp = Directory("${(await getTemporaryDirectory()).path}/data"); + if (temp.existsSync()) { + temp.deleteSync(); + } + temp.createSync(recursive: true); + final archive = ZipDecoder().decodeBuffer(InputFileStream(filePath)); + for (final file in archive.files) { + if (!file.isFile) { + logger.d(file.name); + continue; + } + file.writeContent(OutputFileStream("${temp.path}/${file.name}")); + } + for (final e in temp.listSync()) { + logger.d(e.path); + if (e is! File) continue; + await importWallet(data: e.readAsStringSync()); + } + } + /// Loads and returns a single [Wallet] by name static Future loadWallet(String name) async { final path = @@ -76,4 +200,11 @@ class WalletManager { Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); File("${path.path}/${w.name}").deleteSync(); } + + /// Checks if the wallet exists + static Future exists(String name) async { + return File( + "${(await getApplicationDocumentsDirectory()).path}/wallets/$name", + ).existsSync(); + } } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 0068830..489ffed 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -91,5 +91,18 @@ "evenMoney":"Váš stav je stejný jako minulý měsíc.", "balanceStatusA": "Váš stav je ", "balanceStatusB": " oproti minulému měsíci.", - "searchLabel":"Prohledat záznamy..." + "searchLabel":"Prohledat záznamy...", + "exportSingle":"Exportovat peněženku", + "exportSingleDesc":"Uloží vybranou peněženku do složky Stažené", + "exportArchive":"Exportovat archiv", + "exportArchiveDesc":"Exportuje všechny peněženky do archivu do složky Stažené", + "importSingle":"Importovat peněženku", + "importSingleDesc":"Importuje jednu peněženku ze souboru", + "importArchive":"Importovat archiv", + "importArchiveDesc":"Importuje všechny nepoškozené peněženky z archivu", + "settingsData":"Data", + "selectExportWallet":"Zvolte peněženku k exportování", + "exportError":"Při exportování peněženky nastala chyba", + "exportCompleted":"Export dokončen", + "importCompleted":"Import dokončen" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dbc73a3..e65d3a6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -207,5 +207,18 @@ "evenMoney":"You're on the same balance as last month.", "balanceStatusA": "Your balance is ", "balanceStatusB": " compared to last month.", - "searchLabel":"Search entries..." + "searchLabel":"Search entries...", + "exportSingle":"Export a wallet", + "exportSingleDesc":"Saves a single wallet into your downloads directory", + "exportArchive":"Export archive", + "exportArchiveDesc":"Exports all wallets into an archive to your downloads directory", + "importSingle":"Import a wallet", + "importSingleDesc":"Imports a single wallet from file", + "importArchive":"Import archive", + "importArchiveDesc":"Imports all valid wallets inside an archive", + "settingsData":"Data", + "selectExportWallet":"Select a wallet to export", + "exportError":"An error occured trying to export wallet", + "exportCompleted":"Export completed", + "importCompleted":"Import completed" } \ No newline at end of file diff --git a/lib/views/home.dart b/lib/views/home.dart index d5a13fc..2bc7a28 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -5,12 +5,12 @@ import 'dart:async'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:grouped_list/grouped_list.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; @@ -125,18 +125,15 @@ class _HomeViewState extends State { SpeedDialChild( child: const Icon(Icons.camera_alt), label: AppLocalizations.of(context).addCamera, - onTap: () async { - final picker = ImagePicker(); - final media = - await picker.pickImage(source: ImageSource.camera); - logger.i(media?.name); + onTap: () { + startOcr(SourceType.camera); }, ), SpeedDialChild( child: const Icon(Icons.image), label: AppLocalizations.of(context).addGallery, onTap: () { - startOcr(ImageSource.gallery); + startOcr(SourceType.photoLibrary); }, ), ], @@ -217,6 +214,7 @@ class _HomeViewState extends State { ), ) .then((value) async { + wallets = await WalletManager.listWallets(); selectedWallet = await WalletManager.loadWallet(selectedWallet!.name); }); @@ -593,7 +591,7 @@ class _HomeViewState extends State { ); } - Future startOcr(ImageSource imgSrc) async { + Future startOcr(SourceType sourceType) async { final availableLanguages = await TessdataApi.getDownloadedData(); if (availableLanguages.isEmpty) { if (!mounted) return; @@ -637,9 +635,11 @@ class _HomeViewState extends State { actions: [ TextButton( onPressed: () async { - final picker = ImagePicker(); - final media = await picker.pickImage(source: imgSrc); - if (media == null) { + final filePath = await FlutterFileDialog.pickFile( + params: OpenFileDialogParams( + dialogType: OpenFileDialogType.image, + sourceType: sourceType)); + if (filePath == null) { if (mounted) Navigator.of(context).pop(); return; } @@ -663,7 +663,7 @@ class _HomeViewState extends State { ), ); final string = await FlutterTesseractOcr.extractText( - media.path, + filePath, language: selected, args: { "psm": "4", @@ -758,23 +758,4 @@ class _HomeViewState extends State { ), ); } - - Future getLostData() async { - final picker = ImagePicker(); - final response = await picker.retrieveLostData(); - if (response.isEmpty) { - return; - } - final files = response.files; - if (files != null) { - logger.i("Found lost files"); - _handleLostFiles(files); - } else { - logger.e(response.exception); - } - } - - void _handleLostFiles(List files) { - // TODO: implement - } } diff --git a/lib/views/settings/settings.dart b/lib/views/settings/settings.dart index 0cce304..92e0ba0 100644 --- a/lib/views/settings/settings.dart +++ b/lib/views/settings/settings.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/main.dart'; +import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/util/show_message.dart'; import 'package:prasule/views/settings/edit_categories.dart'; import 'package:prasule/views/settings/graph_type.dart'; import 'package:prasule/views/settings/tessdata_list.dart'; @@ -103,6 +107,190 @@ class _SettingsViewState extends State { ), ], ), + SettingsSection( + title: Text(AppLocalizations.of(context).settingsData), + tiles: [ + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).exportSingle), + description: + Text(AppLocalizations.of(context).exportSingleDesc), + onPressed: (ctx) async { + final all = await WalletManager.listWallets(); + if (!ctx.mounted) return; + final w = await showAdaptiveDialog( + context: ctx, + builder: (ctx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).selectExportWallet, + ), + actions: [ + PlatformButton( + text: AppLocalizations.of(context).cancel, + onPressed: () => Navigator.of(ctx).pop(), + ), + ], + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.7, + height: MediaQuery.of(context).size.height * 0.3, + child: ListView.builder( + itemBuilder: (con, i) => InkWell( + onTap: () => Navigator.of(ctx).pop(all[i].name), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + all[i].name, + textAlign: TextAlign.center, + ), + ), + ), + shrinkWrap: true, + itemCount: all.length, + ), + ), + ), + ); + if (w == null) return; + try { + await WalletManager.exportWallet(name: w); + } catch (e) { + if (!context.mounted) return; + unawaited( + showAdaptiveDialog( + context: context, + builder: (ctx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).exportError, + ), + content: SingleChildScrollView( + child: Flexible( + child: Text(e.toString()), + ), + ), + ), + ), + ); + logger.e(e); + return; + } + if (!ctx.mounted) return; + unawaited( + showMessage( + AppLocalizations.of(ctx).exportCompleted, + ctx, + ), + ); + }, + ), + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).exportArchive), + description: + Text(AppLocalizations.of(context).exportArchiveDesc), + onPressed: (ctx) async { + try { + await WalletManager.exportAllWallets(); + } catch (e) { + if (!ctx.mounted) return; + unawaited( + showAdaptiveDialog( + context: context, + builder: (ctx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).exportError, + ), + content: SingleChildScrollView( + child: Flexible( + child: Text(e.toString()), + ), + ), + ), + ), + ); + logger.e(e); + return; + } + if (!ctx.mounted) return; + unawaited( + showMessage( + AppLocalizations.of(ctx).exportCompleted, + context, + ), + ); + }, + ), + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).importSingle), + description: + Text(AppLocalizations.of(context).importSingleDesc), + onPressed: (ctx) async { + try { + await WalletManager.importWallet(); + } catch (e) { + if (!ctx.mounted) return; + unawaited( + showAdaptiveDialog( + context: context, + builder: (ctx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).exportError, + ), + content: SingleChildScrollView( + child: Flexible( + child: Text(e.toString()), + ), + ), + ), + ), + ); + logger.e(e); + return; + } + if (!ctx.mounted) return; + unawaited( + showMessage( + AppLocalizations.of(ctx).importCompleted, + context, + ), + ); + }, + ), + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).importArchive), + description: + Text(AppLocalizations.of(context).importArchiveDesc), + onPressed: (ctx) async { + try { + await WalletManager.importArchive(); + } catch (e) { + if (!ctx.mounted) return; + unawaited( + showAdaptiveDialog( + context: context, + builder: (ctx) => AlertDialog.adaptive( + title: Text( + AppLocalizations.of(context).exportError, + ), + content: SingleChildScrollView( + child: Flexible( + child: Text(e.toString()), + ), + ), + ), + ), + ); + logger.e(e); + return; + } + if (!ctx.mounted) return; + unawaited( + showMessage( + AppLocalizations.of(ctx).importCompleted, + context, + ), + ); + }, + ), + ], + ), ], ), ); diff --git a/pubspec.lock b/pubspec.lock index 759eca0..271729b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -18,7 +18,7 @@ packages: source: hosted version: "6.3.0" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" @@ -177,14 +177,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.7.2" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e - url: "https://pub.dev" - source: hosted - version: "0.3.3+8" crypto: dependency: transitive description: @@ -273,38 +265,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" - url: "https://pub.dev" - source: hosted - version: "0.9.2+1" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 - url: "https://pub.dev" - source: hosted - version: "0.9.3+3" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 - url: "https://pub.dev" - source: hosted - version: "0.9.3+1" fixnum: dependency: transitive description: @@ -347,6 +307,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_file_dialog: + dependency: "direct main" + description: + name: flutter_file_dialog + sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_iconpicker: dependency: "direct main" description: @@ -424,14 +392,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da - url: "https://pub.dev" - source: hosted - version: "2.0.17" flutter_slidable: dependency: "direct main" description: @@ -519,14 +479,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.2" - http: - dependency: transitive - description: - name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba - url: "https://pub.dev" - source: hosted - version: "1.2.0" http_multi_server: dependency: transitive description: @@ -551,70 +503,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.4" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" - url: "https://pub.dev" - source: hosted - version: "1.0.7" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" - url: "https://pub.dev" - source: hosted - version: "0.8.9+3" - image_picker_for_web: - dependency: transitive - description: - name: image_picker_for_web - sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 - url: "https://pub.dev" - source: hosted - version: "3.0.2" - image_picker_ios: - dependency: transitive - description: - name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 - url: "https://pub.dev" - source: hosted - version: "0.8.9+1" - image_picker_linux: - dependency: transitive - description: - name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_macos: - dependency: transitive - description: - name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" - image_picker_platform_interface: - dependency: transitive - description: - name: image_picker_platform_interface - sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b - url: "https://pub.dev" - source: hosted - version: "2.9.3" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 8f456f7..ca3d000 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ environment: # the latest version available on pub.dev. To see which dependencies have newer # versions available, run `flutter pub outdated`. dependencies: + archive: ^3.4.10 cupertino_icons: ^1.0.2 currency_picker: ^2.0.16 dio: ^5.3.0 @@ -21,6 +22,7 @@ dependencies: flex_color_picker: ^3.3.0 flutter: sdk: flutter + flutter_file_dialog: ^3.0.2 flutter_iconpicker: ^3.2.4 flutter_localizations: sdk: flutter @@ -29,7 +31,6 @@ dependencies: flutter_tesseract_ocr: ^0.4.23 fluttertoast: ^8.2.4 grouped_list: ^5.1.2 - image_picker: ^1.0.1 intl: any introduction_screen: ^3.1.11 json_annotation: ^4.8.1 -- 2.45.2 From 2144a7cddc2087769a03d915c14adda94614a318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 23:33:26 +0100 Subject: [PATCH 8/9] docs: remove duplicate badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bcf9222..ae2f379 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # prasule -[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) +[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) Expense manager -- 2.45.2 From f02be7b2d39ad3c92faf9d585c138edb05b37a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 22 Jan 2024 23:33:57 +0100 Subject: [PATCH 9/9] docs: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b77505..e947fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Add tests - Add searching through entries to homepage - Replace `PlatformDialog` with `AlertDialog.adaptive` +- Add import/export of wallets # 1.0.0-alpha+4 - Fix OCR downloads # 1.0.0-alpha+3 -- 2.45.2