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/.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/CHANGELOG.md b/CHANGELOG.md index 2e0114d..e947fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ + +# 1.0.0-alpha+5 +- 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 diff --git a/README.md b/README.md index 34e9750..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/) +[![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 diff --git a/android/app/build.gradle b/android/app/build.gradle index 905ffcc..b3e80a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -76,4 +76,6 @@ flutter { source '../..' } -dependencies {} +dependencies { + implementation 'com.android.support:multidex:1.0.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"> 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 0b3cec5..e2dcd1f 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'; @@ -156,9 +160,44 @@ 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", + entries: [], + recurringEntries: [], + categories: [ + WalletCategory( + name: "Default", + id: 0, + icon: IconData( + Icons.payments.codePoint, + fontFamily: 'MaterialIcons', + ), + color: Colors.white, + ), + ], currency: Currency.from( json: { "code": "USD", @@ -175,4 +214,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..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'; @@ -24,10 +28,143 @@ 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(); + } + } + + /// 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 = @@ -63,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 d1794ff..489ffed 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -87,5 +87,22 @@ "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.", + "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 c2fe305..e65d3a6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -203,5 +203,22 @@ "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.", + "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/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/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/pw/platformfield.dart b/lib/pw/platformfield.dart index 710e534..c45a0c0 100644 --- a/lib/pw/platformfield.dart +++ b/lib/pw/platformfield.dart @@ -21,6 +21,8 @@ class PlatformField extends PlatformWidget { 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/util/graphs.dart b/lib/util/graphs.dart index 9e2ed5b..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 @@ -522,7 +522,7 @@ class Indicator extends StatelessWidget { /// Text shown next to the indicator circle final String text; - /// Style for the text + /// Text style of the indicator final TextStyle textStyle; @override diff --git a/lib/views/home.dart b/lib/views/home.dart index 3420864..2bc7a28 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,29 +1,27 @@ // 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'; 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'; 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'; 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'; @@ -46,6 +44,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(); @@ -75,399 +76,529 @@ 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!.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", + 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!), + ), ); - - 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, - ), - ); + if (sw != null) { + selectedWallet = sw; } - - logger.d( - "Created ${selectedWallet!.recurringEntries.length} recurring entries", - ); - - // save and reload - WalletManager.saveWallet(selectedWallet!).then((value) { - Navigator.of(context).pushReplacement( - platformRoute( - (p0) => const HomeView(), - ), - ); - }); + 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: () { + startOcr(SourceType.camera); + }, ), - DropdownMenuItem( - value: -1, - child: Text(AppLocalizations.of(context).newWallet), + SpeedDialChild( + child: const Icon(Icons.image), + label: AppLocalizations.of(context).addGallery, + onTap: () { + startOcr(SourceType.photoLibrary); + }, ), ], - 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(), - ), - ], - ) - : (selectedWallet!.entries.isEmpty) - ? Column( - children: [ - Text( - AppLocalizations.of(context).noEntries, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - Text( - AppLocalizations.of(context).noEntriesSub, - ), - ], - ) - : GroupedListView( - groupHeaderBuilder: (element) => Text( - DateFormat.yMMMM(locale).format(element.date), - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), + 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, ), - 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(), - ), + ), + ); + 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(), + ), + ) + .then((value) async { + wallets = await WalletManager.listWallets(); + 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, ), ), - title: Text(element.data.name), - subtitle: RichText( - text: TextSpan( + Text( + AppLocalizations.of(context).noEntriesSub, + ), + ], + ) + : Overlay( + initialEntries: [ + OverlayEntry( + builder: (context) => Column( children: [ - TextSpan( - text: NumberFormat.currency( + Text( + NumberFormat.compactCurrency( + locale: locale, 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, - ), + ).format( + selectedWallet!.calculateCurrentBalance(), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, ), ), - TextSpan( - text: - " | ${DateFormat.MMMd(locale).format(element.date)}", - style: TextStyle( - color: Theme.of(context) - .colorScheme - .background - .calculateTextColor(), + 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) { + showAdaptiveDialog( + context: context, + builder: (cx) => + AlertDialog.adaptive( + title: Text( + 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(); + }, + ), + ), + ), + ], ), - ), + ), ), ), ); } - Future startOcr(ImageSource imgSrc) async { + Future startOcr(SourceType sourceType) async { 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, @@ -497,16 +628,18 @@ 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 { - 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; } @@ -521,16 +654,16 @@ 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, ), ); final string = await FlutterTesseractOcr.extractText( - media.path, + filePath, language: selected, args: { "psm": "4", @@ -556,6 +689,7 @@ class _HomeViewState extends State { } description.write("${line.replaceAll(regex, "")}\n"); } + if (!ctx.mounted) return; Navigator.of(ctx).pop(); // show edit final newEntry = @@ -591,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( @@ -624,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/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 832234d..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'; @@ -146,10 +145,10 @@ class _EditCategoriesViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; - await showDialog( + if (!context.mounted) return; + 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 { @@ -227,7 +226,7 @@ class _EditCategoriesViewState extends State { await WalletManager.saveWallet( selectedWallet!, ); - if (!mounted) return; + if (!context.mounted) return; Navigator.of(context).pop(); }, child: Text( @@ -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 cc5d57e..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( @@ -70,7 +69,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 +89,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(() {}); }, @@ -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( @@ -128,7 +127,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 +147,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/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/lib/views/settings/tessdata_list.dart b/lib/views/settings/tessdata_list.dart index a0648a5..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), ), @@ -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( @@ -96,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 e8749e3..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: [ @@ -297,10 +303,10 @@ class _SetupViewState extends State { (await SharedPreferences.getInstance()) .getBool("useMaterialYou") ?? false; - if (!mounted) return; - await showDialog( + if (!context.mounted) return; + 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( diff --git a/pubspec.lock b/pubspec.lock index 30c6e96..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" @@ -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: @@ -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: "0aa47a725c346825a2bd396343ce63ac00bda6eff2fbc43eabe99737dede8262" - url: "https://pub.dev" - source: hosted - version: "2.6.1" - 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: @@ -325,10 +285,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: @@ -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: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139 - url: "https://pub.dev" - source: hosted - version: "1.1.2" http_multi_server: dependency: transitive description: @@ -547,74 +499,10 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" url: "https://pub.dev" source: hosted - version: "4.1.3" - image_picker: - dependency: "direct main" - description: - name: image_picker - sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b" - url: "https://pub.dev" - source: hosted - version: "1.0.6" - image_picker_android: - dependency: transitive - description: - name: image_picker_android - sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec" - url: "https://pub.dev" - source: hosted - version: "0.8.9+2" - 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: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf - url: "https://pub.dev" - source: hosted - version: "0.8.9" - 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: "0e827c156e3a90edd3bbe7f6de048b39247b16e58173b08a835b7eb00aba239e" - url: "https://pub.dev" - source: hosted - version: "2.9.2" - image_picker_windows: - dependency: transitive - description: - name: image_picker_windows - sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" - url: "https://pub.dev" - source: hosted - version: "0.2.1+1" + version: "4.1.4" integration_test: dependency: "direct dev" description: flutter @@ -672,18 +560,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 +608,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 +664,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 +688,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 +704,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 +728,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 +744,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 +760,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 +816,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 +832,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 +1053,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: diff --git a/pubspec.yaml b/pubspec.yaml index cd1ca24..ca3d000 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' @@ -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