From 574180c5f2c451ed4b80d87e16e01ca0c8758bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Wed, 1 Nov 2023 18:39:21 +0100 Subject: [PATCH] feat: allow creating new wallets + fix bugs --- integration_test/setup_test.dart | 1 - lib/api/walletmanager.dart | 12 +- lib/l10n/app_cs.arb | 7 +- lib/l10n/app_en.arb | 4 +- lib/views/home.dart | 194 ++++++++++++++++++------------- lib/views/setup.dart | 110 +++++++++++------- 6 files changed, 199 insertions(+), 129 deletions(-) diff --git a/integration_test/setup_test.dart b/integration_test/setup_test.dart index ff269e7..db9ca13 100644 --- a/integration_test/setup_test.dart +++ b/integration_test/setup_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/lib/api/walletmanager.dart b/lib/api/walletmanager.dart index 9fc36ec..d4432fc 100644 --- a/lib/api/walletmanager.dart +++ b/lib/api/walletmanager.dart @@ -5,13 +5,17 @@ import 'package:path_provider/path_provider.dart'; import 'package:prasule/api/wallet.dart'; class WalletManager { - static Future> listWallets() async { + static Future> listWallets() async { var path = Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); if (!path.existsSync()) { path.createSync(); } - return path.listSync().map((e) => e.path.split("/").last).toList(); + var wallets = []; + for (var w in path.listSync().map((e) => e.path.split("/").last).toList()) { + wallets.add(await loadWallet(w)); + } + return wallets; } static Future loadWallet(String name) async { @@ -27,14 +31,16 @@ class WalletManager { return Wallet.fromJson(jsonDecode(wallet.readAsStringSync())); } - static Future saveWallet(Wallet w) async { + static Future saveWallet(Wallet w) async { var path = Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); var wallet = File("${path.path}/${w.name}"); if (!path.existsSync()) { path.createSync(); } + if (wallet.existsSync()) return false; wallet.writeAsStringSync(jsonEncode(w.toJson())); + return true; } static Future deleteWallet(Wallet w) async { diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index a06d8fa..b078760 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -7,7 +7,7 @@ "back": "Zpět", "finish": "Dokončit", "errorEmptyName": "Název nemůže být prázdný!", - "welcome": "Vítej!", + "welcome": "Vítejte!", "welcomeAboutPrasule": "Prašule je správce výdajů navržený pro lidi, kteří nechtějí vyplňovat každý malý detail.", "welcomeInstruction": "Na této obrazovce si nastavíte svoji 'peněženku', ve které budou zaznamenány vaše výdaje uspořádané do kategorií, které si nastavíte hned potom.", "setupWalletNameCurrency": "Nastavte si název a měnu peněženky", @@ -53,5 +53,8 @@ "langDownloadProgress": "Postup: $progress %", "addingFromOcr": "Přidat skrz OCR", "license":"©️ 2023 Matyáš Caras\nVydáno pod licencí GNU AGPL license verze 3", - "description":"Popis" + "description":"Popis", + "newWallet":"Přidat novou peněženku", + "walletExists":"Peněženka s tímto názvem již existuje!" + } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6346d17..d131587 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -89,5 +89,7 @@ }, "addingFromOcr": "Add from OCR", "license":"©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3", - "description":"Description" + "description":"Description", + "newWallet":"Add new wallet", + "walletExists":"A wallet with this name already exists!" } \ No newline at end of file diff --git a/lib/views/home.dart b/lib/views/home.dart index 8fea23e..eedd02b 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -31,6 +31,7 @@ class HomeView extends StatefulWidget { class _HomeViewState extends State { Wallet? selectedWallet; + List wallets = []; DateTime? prevDate; late String locale; @override @@ -47,13 +48,13 @@ class _HomeViewState extends State { } void loadWallet() async { - var wallets = await WalletManager.listWallets(); + wallets = await WalletManager.listWallets(); if (wallets.isEmpty && mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (c) => const SetupView())); return; } - selectedWallet = await WalletManager.loadWallet(wallets.first); + selectedWallet = wallets.first; setState(() {}); } @@ -98,7 +99,42 @@ class _HomeViewState extends State { ], ), appBar: AppBar( - title: Text(AppLocalizations.of(context)!.home), + title: DropdownButton( + value: + (selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!), + items: [ + ...wallets.map( + (e) => DropdownMenuItem( + value: wallets.indexOf( + e, + ), + child: Text(e.name), + ), + ), + DropdownMenuItem( + value: -1, + child: Text(AppLocalizations.of(context)!.newWallet), + ) + ], + onChanged: (v) async { + if (v == null || v == -1) { + Navigator.of(context).push( + platformRoute( + (c) => const SetupView( + newWallet: true, + ), + ), + ); + wallets = await WalletManager.listWallets(); + logger.i(wallets.length); + selectedWallet = wallets.last; + setState(() {}); + return; + } + selectedWallet = wallets[v]; + setState(() {}); + }, + ), actions: [ PopupMenuButton( itemBuilder: (context) => [ @@ -284,87 +320,87 @@ class _HomeViewState extends State { builder: (ctx, setState) => PlatformDialog( actions: [ TextButton( - onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? media = await picker.pickImage(source: imgSrc); - if (media == null) { - if (mounted) Navigator.of(context).pop(); - return; - } - // get selected languages - var selected = availableLanguages - .where((element) => selectedLanguages[ - availableLanguages.indexOf(element)]) - .join("+") - .replaceAll(".traineddata", ""); - logger.i(selected); - if (!mounted) return; - showDialog( - context: context, - builder: (c) => PlatformDialog( - title: AppLocalizations.of(context)!.ocrLoading), - barrierDismissible: false); - var string = await FlutterTesseractOcr.extractText(media.path, - language: selected, - args: { - "psm": "4", - "preserve_interword_spaces": "1", - }); - if (!mounted) return; - Navigator.of(context).pop(); - logger.i(string); - if (!mounted) return; - var lines = string.split("\n") - ..removeWhere((element) { - element.trim(); - return element.isEmpty; + onPressed: () async { + final ImagePicker picker = ImagePicker(); + final XFile? media = await picker.pickImage(source: imgSrc); + if (media == null) { + if (mounted) Navigator.of(context).pop(); + return; + } + // get selected languages + var selected = availableLanguages + .where((element) => + selectedLanguages[availableLanguages.indexOf(element)]) + .join("+") + .replaceAll(".traineddata", ""); + logger.i(selected); + if (!mounted) return; + showDialog( + context: context, + builder: (c) => PlatformDialog( + title: AppLocalizations.of(context)!.ocrLoading), + barrierDismissible: false); + var string = await FlutterTesseractOcr.extractText(media.path, + language: selected, + args: { + "psm": "4", + "preserve_interword_spaces": "1", }); - var price = 0.0; - var description = ""; - for (var line in lines) { - // find numbered prices on each line - var regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+'); - for (var match in regex.allMatches(line)) { - price += double.tryParse(match.group(0).toString()) ?? 0; - } - description += "${line.replaceAll(regex, "")}\n"; + if (!mounted) return; + Navigator.of(context).pop(); + logger.i(string); + if (!mounted) return; + var lines = string.split("\n") + ..removeWhere((element) { + element.trim(); + return element.isEmpty; + }); + var price = 0.0; + var description = ""; + for (var line in lines) { + // find numbered prices on each line + var regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+'); + for (var match in regex.allMatches(line)) { + price += double.tryParse(match.group(0).toString()) ?? 0; } - Navigator.of(ctx).pop(); - // show edit - Navigator.of(context) - .push( - platformRoute( - (c) => CreateEntryView( - w: selectedWallet!, - editEntry: WalletSingleEntry( - data: EntryData( - name: "", - amount: price, - description: description), - type: EntryType.expense, - date: DateTime.now(), - category: selectedWallet!.categories.first, - id: selectedWallet!.nextId, - ), + description += "${line.replaceAll(regex, "")}\n"; + } + Navigator.of(ctx).pop(); + // show edit + Navigator.of(context) + .push( + platformRoute( + (c) => CreateEntryView( + w: selectedWallet!, + editEntry: WalletSingleEntry( + data: EntryData( + name: "", amount: price, description: description), + type: EntryType.expense, + date: DateTime.now(), + category: selectedWallet!.categories.first, + id: selectedWallet!.nextId, ), ), - ) - .then( - (newEntry) { - // save entry if we didn't return empty - if (newEntry == null) return; - selectedWallet!.entries.add(newEntry); - WalletManager.saveWallet(selectedWallet!); - setState(() {}); - }, - ); - }, - child: const Text("Ok")), + ), + ) + .then( + (newEntry) { + // save entry if we didn't return empty + if (newEntry == null) return; + selectedWallet!.entries.add(newEntry); + WalletManager.saveWallet(selectedWallet!); + setState(() {}); + }, + ); + }, + child: const Text("Ok"), + ), TextButton( - onPressed: () { - Navigator.of(c).pop(); - }, - child: const Text("Cancel")), + onPressed: () { + Navigator.of(c).pop(); + }, + child: const Text("Cancel"), + ), ], title: AppLocalizations.of(context)!.ocrSelect, content: Column( diff --git a/lib/views/setup.dart b/lib/views/setup.dart index c1aaba1..feb0ca2 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -12,8 +12,10 @@ import 'package:prasule/views/home.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class SetupView extends StatefulWidget { - const SetupView({super.key}); + const SetupView({super.key, this.newWallet = false}); + /// We are only creating a new wallet, no first-time setup + final bool newWallet; @override State createState() => _SetupViewState(); } @@ -36,36 +38,40 @@ class _SetupViewState extends State { var name = ""; @override - void initState() { - super.initState(); - categories = [ - WalletCategory( - name: AppLocalizations.of(context)!.categoryHealth, - type: EntryType.expense, - id: 1, - icon: IconData(Icons.medical_information.codePoint, - fontFamily: 'MaterialIcons'), - ), - WalletCategory( - name: AppLocalizations.of(context)!.categoryCar, - type: EntryType.expense, - id: 2, - icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), - ), - WalletCategory( - name: AppLocalizations.of(context)!.categoryFood, - type: EntryType.expense, - id: 3, - icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), - ), - WalletCategory( - name: AppLocalizations.of(context)!.categoryTravel, - type: EntryType.expense, - id: 4, - icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), - ), - ]; - setState(() {}); + void didChangeDependencies() { + super.didChangeDependencies(); + if (categories.isEmpty) { + categories = [ + WalletCategory( + name: AppLocalizations.of(context)!.categoryHealth, + type: EntryType.expense, + id: 1, + icon: IconData(Icons.medical_information.codePoint, + fontFamily: 'MaterialIcons'), + ), + WalletCategory( + name: AppLocalizations.of(context)!.categoryCar, + type: EntryType.expense, + id: 2, + icon: + IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), + ), + WalletCategory( + name: AppLocalizations.of(context)!.categoryFood, + type: EntryType.expense, + id: 3, + icon: + IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), + ), + WalletCategory( + name: AppLocalizations.of(context)!.categoryTravel, + type: EntryType.expense, + id: 4, + icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), + ), + ]; + setState(() {}); + } } @override @@ -99,11 +105,27 @@ class _SetupViewState extends State { currency: _selectedCurrency, categories: categories); WalletManager.saveWallet(wallet).then( - (value) => Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (c) => const HomeView(), - ), - ), + (value) { + if (!value) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(AppLocalizations.of(context)!.walletExists), + ), + ); + return; + } + if (widget.newWallet) { + Navigator.of(context).pop(); + return; + } + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (c) => const HomeView(), + ), + ); + }, ); }, pages: [ @@ -123,13 +145,15 @@ class _SetupViewState extends State { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Flexible( - child: Text( - AppLocalizations.of(context)!.welcomeAboutPrasule), - ), - const SizedBox( - height: 5, - ), + if (!widget.newWallet) + Flexible( + child: Text( + AppLocalizations.of(context)!.welcomeAboutPrasule), + ), + if (!widget.newWallet) + const SizedBox( + height: 5, + ), Flexible( child: Text( AppLocalizations.of(context)!.welcomeInstruction),