diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adbf2b..2f52790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 1.0.0-alpha+3 - Add settings view for editing wallet categories - Change code according to more aggressive linting +- Create a default "no category" category, mainly to store entries with removed categories # 1.0.0-alpha+2 - Fixed localization issues - Added graphs for expenses and income per month/year diff --git a/lib/api/category.dart b/lib/api/category.dart index 0a2e16f..30424f7 100644 --- a/lib/api/category.dart +++ b/lib/api/category.dart @@ -9,19 +9,14 @@ class WalletCategory { /// Represents a category in a user's wallet WalletCategory({ required this.name, - required this.type, required this.id, required this.icon, }); - /// Connect the generated [_$WalletEntry] function to the `fromJson` - /// factory. + /// Connects generated fromJson method factory WalletCategory.fromJson(Map json) => _$WalletCategoryFromJson(json); - /// Expense or income - final EntryType type; - /// User-defined name String name; @@ -32,7 +27,7 @@ class WalletCategory { @JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson) IconData icon; - /// Connect the generated [_$PersonToJson] function to the `toJson` method. + /// Connects generated toJson method Map toJson() => _$WalletCategoryToJson(this); @override diff --git a/lib/api/category.g.dart b/lib/api/category.g.dart index 00f6a92..f70475f 100644 --- a/lib/api/category.g.dart +++ b/lib/api/category.g.dart @@ -9,20 +9,13 @@ part of 'category.dart'; WalletCategory _$WalletCategoryFromJson(Map json) => WalletCategory( name: json['name'] as String, - type: $enumDecode(_$EntryTypeEnumMap, json['type']), id: json['id'] as int, icon: _iconDataFromJson(json['icon'] as Map), ); Map _$WalletCategoryToJson(WalletCategory instance) => { - 'type': _$EntryTypeEnumMap[instance.type]!, 'name': instance.name, 'id': instance.id, 'icon': _iconDataToJson(instance.icon), }; - -const _$EntryTypeEnumMap = { - EntryType.expense: 'expense', - EntryType.income: 'income', -}; diff --git a/lib/api/entry_data.dart b/lib/api/entry_data.dart index 21e8f59..23281f2 100644 --- a/lib/api/entry_data.dart +++ b/lib/api/entry_data.dart @@ -7,7 +7,7 @@ class EntryData { /// Contains raw data EntryData({required this.name, required this.amount, this.description = ""}); - /// Connects generated fromJson function + /// Connects generated fromJson method factory EntryData.fromJson(Map json) => _$EntryDataFromJson(json); @@ -20,6 +20,6 @@ class EntryData { /// Amount for entry double amount; - /// Connects generated toJson function + /// Connects generated toJson method Map toJson() => _$EntryDataToJson(this); } diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index ed6526b..e38b844 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -2,6 +2,7 @@ import 'package:currency_picker/currency_picker.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/walletentry.dart'; +import 'package:prasule/api/walletmanager.dart'; part 'wallet.g.dart'; Currency _currencyFromJson(Map data) => @@ -13,14 +14,15 @@ Currency _currencyFromJson(Map data) => @JsonSerializable() class Wallet { /// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s - Wallet( - {required this.name, - required this.currency, - this.categories = const [], - this.entries = const [], - this.starterBalance = 0,}); + Wallet({ + required this.name, + required this.currency, + this.categories = const [], + this.entries = const [], + this.starterBalance = 0, + }); - /// Connects generated fromJson function + /// Connects generated fromJson method factory Wallet.fromJson(Map json) => _$WalletFromJson(json); /// Name of the wallet @@ -41,10 +43,10 @@ class Wallet { @JsonKey(fromJson: _currencyFromJson) final Currency currency; - /// Connects generated toJson function + /// Connects generated toJson method Map toJson() => _$WalletToJson(this); - /// Getter for the next unused unique number ID in the wallet's entry list + /// Getter for the next unused unique number ID in the wallet's **entry** list int get nextId { var id = 1; while (entries.where((element) => element.id == id).isNotEmpty) { @@ -53,6 +55,33 @@ class Wallet { return id; } + /// Getter for the next unused unique number ID in the wallet's **category** + /// list + int get nextCategoryId { + var id = 0; + while (categories.where((element) => element.id == id).isNotEmpty) { + id++; // create unique ID + } + return id; + } + + /// Removes the specified category. + /// + /// All [WalletSingleEntry]s will have their category reassigned + /// to the default *No category* + Future removeCategory(WalletCategory category) async { + // First remove the category from existing entries + for (final entryToChange + in entries.where((element) => element.category.id == category.id)) { + entryToChange.category = + categories.where((element) => element.id == 0).first; + } + // Remove the category + categories.removeWhere((element) => element.id == category.id); + // Save + await WalletManager.saveWallet(this); + } + /// Empty wallet used for placeholders static final Wallet empty = Wallet( name: "Empty", diff --git a/lib/api/walletentry.dart b/lib/api/walletentry.dart index 878c023..a515fc5 100644 --- a/lib/api/walletentry.dart +++ b/lib/api/walletentry.dart @@ -9,14 +9,15 @@ part 'walletentry.g.dart'; /// This is an entry containing a single item class WalletSingleEntry { /// This is an entry containing a single item - WalletSingleEntry( - {required this.data, - required this.type, - required this.date, - required this.category, - required this.id,}); + WalletSingleEntry({ + required this.data, + required this.type, + required this.date, + required this.category, + required this.id, + }); - /// Connects generated fromJson function + /// Connects generated fromJson method factory WalletSingleEntry.fromJson(Map json) => _$WalletSingleEntryFromJson(json); @@ -35,6 +36,6 @@ class WalletSingleEntry { /// Unique entry ID int id; - /// Connects generated toJson function + /// Connects generated toJson method Map toJson() => _$WalletSingleEntryToJson(this); } diff --git a/lib/api/walletmanager.dart b/lib/api/walletmanager.dart index d816b46..e2c66ab 100644 --- a/lib/api/walletmanager.dart +++ b/lib/api/walletmanager.dart @@ -54,7 +54,6 @@ class WalletManager { } // if (!wallet.existsSync()) return false; wallet.writeAsStringSync(jsonEncode(w.toJson())); - logger.i(wallet.existsSync()); return true; } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 13fc0a3..c3065c1 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -76,6 +76,8 @@ "enableYou":"Povolit Material You (Může vyžadovat restart aplikace)", "enableYouDesc":"Aplikace použije barevné schéma z vaší tapety", "editCategories":"Upravit kategorie", - "editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky" + "editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky", + "wallet":"Peněženka", + "noCategory":"Žádná kategorie" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8aeea34..eb1bf55 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -156,5 +156,7 @@ "enableYou":"Enable Material You (May require an app restart)", "enableYouDesc":"The app will use a color scheme from your wallpaper", "editCategories":"Edit categories", - "editCategoriesDesc":"Add, edit or remove categories from a wallet" + "editCategoriesDesc":"Add, edit or remove categories from a wallet", + "wallet":"Wallet", + "noCategory":"No category" } \ No newline at end of file diff --git a/lib/views/home.dart b/lib/views/home.dart index 868cdae..430ccb1 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -198,11 +198,16 @@ class _HomeViewState extends State { ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), onSelected: (value) { if (value == AppLocalizations.of(context).settings) { - Navigator.of(context).push( + 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, diff --git a/lib/views/settings/edit_categories.dart b/lib/views/settings/edit_categories.dart index 85a8d2e..b97afc0 100644 --- a/lib/views/settings/edit_categories.dart +++ b/lib/views/settings/edit_categories.dart @@ -27,7 +27,6 @@ class EditCategoriesView extends StatefulWidget { class _EditCategoriesViewState extends State { Wallet? selectedWallet; List wallets = []; - List categories = []; @override void initState() { @@ -45,7 +44,7 @@ class _EditCategoriesViewState extends State { return; } selectedWallet = wallets.first; - categories = selectedWallet!.categories; + logger.i(selectedWallet!.categories); setState(() {}); } @@ -115,109 +114,130 @@ class _EditCategoriesViewState extends State { ), body: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - AppLocalizations.of(context).setupCategoriesEditHint, - textAlign: TextAlign.center, - ), - SizedBox( - height: MediaQuery.of(context).size.height * 0.64, - child: ListView.builder( - shrinkWrap: true, - itemBuilder: (context, i) => ListTile( - leading: GestureDetector( - onTap: () async { - final icon = - await FlutterIconPicker.showIconPicker(context); - if (icon == null) return; - categories[i].icon = icon; - setState(() {}); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.secondary, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - categories[i].icon, - color: Theme.of(context).colorScheme.onSecondary, - ), - ), + children: selectedWallet == null + ? [const CircularProgressIndicator()] + : [ + Text( + AppLocalizations.of(context).setupCategoriesEditHint, + textAlign: TextAlign.center, + ), + SizedBox( + height: MediaQuery.of(context).size.height * 0.64, + child: ListView.builder( + shrinkWrap: true, + itemBuilder: (context, i) => (i == 0) + ? const SizedBox() + : ListTile( + leading: GestureDetector( + onTap: () async { + final icon = + await FlutterIconPicker.showIconPicker( + context, + ); + if (icon == null) return; + selectedWallet!.categories[i].icon = icon; + await WalletManager.saveWallet(selectedWallet!); + setState(() {}); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: + Theme.of(context).colorScheme.secondary, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + selectedWallet!.categories[i].icon, + color: Theme.of(context) + .colorScheme + .onSecondary, + ), + ), + ), + ), + trailing: IconButton( + icon: const Icon(Icons.cancel), + onPressed: () async { + await selectedWallet!.removeCategory( + selectedWallet!.categories[i], + ); + setState(() {}); + }, + ), + title: GestureDetector( + onTap: () { + final controller = TextEditingController( + text: selectedWallet!.categories[i].name, + ); + showDialog( + context: context, + builder: (c) => PlatformDialog( + actions: [ + TextButton( + onPressed: () async { + if (controller.text.isEmpty) return; + selectedWallet!.categories[i].name = + controller.text; + await WalletManager.saveWallet( + selectedWallet!, + ); + if (!mounted) return; + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context).ok, + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context).cancel, + ), + ), + ], + title: AppLocalizations.of(context) + .setupCategoriesEditingName, + content: SizedBox( + width: 400, + child: + PlatformField(controller: controller), + ), + ), + ); + }, + child: Text( + selectedWallet!.categories[i].name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ), + itemCount: selectedWallet!.categories.length, ), ), - trailing: IconButton( - icon: const Icon(Icons.cancel), - onPressed: () { - categories.removeAt(i); - setState(() {}); - }, - ), - title: GestureDetector( - onTap: () { - final controller = - TextEditingController(text: categories[i].name); - showDialog( - context: context, - builder: (c) => PlatformDialog( - actions: [ - TextButton( - onPressed: () { - if (controller.text.isEmpty) return; - categories[i].name = controller.text; - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context).ok), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(AppLocalizations.of(context).cancel), - ), - ], - title: AppLocalizations.of(context) - .setupCategoriesEditingName, - content: SizedBox( - width: 400, - child: PlatformField(controller: controller), + IconButton( + onPressed: () async { + selectedWallet!.categories.add( + WalletCategory( + name: AppLocalizations.of(context) + .setupWalletNamePlaceholder, + id: selectedWallet!.nextCategoryId, + icon: IconData( + Icons.question_mark.codePoint, + fontFamily: 'MaterialIcons', ), ), ); + await WalletManager.saveWallet(selectedWallet!); + setState(() {}); }, - child: Text( - categories[i].name, - style: const TextStyle(fontWeight: FontWeight.bold), - ), + icon: const Icon(Icons.add), ), - ), - itemCount: categories.length, - ), - ), - IconButton( - onPressed: () { - var id = 1; - while ( - categories.where((element) => element.id == id).isNotEmpty) { - id++; // create unique ID - } - categories.add( - WalletCategory( - name: AppLocalizations.of(context).setupWalletNamePlaceholder, - type: EntryType.expense, - id: id, - icon: IconData( - Icons.question_mark.codePoint, - fontFamily: 'MaterialIcons', - ), - ), - ); - setState(() {}); - }, - icon: const Icon(Icons.add), - ), - ], + ], ), ); } diff --git a/lib/views/settings/settings.dart b/lib/views/settings/settings.dart index c1e63e9..0cce304 100644 --- a/lib/views/settings/settings.dart +++ b/lib/views/settings/settings.dart @@ -43,6 +43,7 @@ class _SettingsViewState extends State { ), sections: [ SettingsSection( + title: Text(AppLocalizations.of(context).wallet), tiles: [ SettingsTile.navigation( title: Text(AppLocalizations.of(context).editCategories), diff --git a/lib/views/setup.dart b/lib/views/setup.dart index c288f8f..567fd0a 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -51,9 +51,16 @@ class _SetupViewState extends State { super.didChangeDependencies(); if (categories.isEmpty) { categories = [ + WalletCategory( + name: AppLocalizations.of(context).noCategory, + id: 0, + icon: IconData( + Icons.payments.codePoint, + fontFamily: 'MaterialIcons', + ), + ), WalletCategory( name: AppLocalizations.of(context).categoryHealth, - type: EntryType.expense, id: 1, icon: IconData( Icons.medical_information.codePoint, @@ -62,21 +69,18 @@ class _SetupViewState extends State { ), 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'), ), @@ -269,90 +273,98 @@ class _SetupViewState extends State { height: MediaQuery.of(context).size.height * 0.64, child: ListView.builder( shrinkWrap: true, - itemBuilder: (context, i) => ListTile( - leading: GestureDetector( - onTap: () async { - final icon = - await FlutterIconPicker.showIconPicker( - context, - ); - if (icon == null) return; - categories[i].icon = icon; - setState(() {}); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.secondary, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - categories[i].icon, - color: - Theme.of(context).colorScheme.onSecondary, - ), - ), - ), - ), - trailing: IconButton( - icon: const Icon(Icons.cancel), - onPressed: () { - categories.removeAt(i); - setState(() {}); - }, - ), - title: GestureDetector( - onTap: () { - final controller = TextEditingController( - text: categories[i].name, - ); - showDialog( - context: context, - builder: (c) => PlatformDialog( - actions: [ - TextButton( - onPressed: () { - if (controller.text.isEmpty) return; - categories[i].name = controller.text; - Navigator.of(context).pop(); - }, - child: Text( - AppLocalizations.of(context).ok, + itemBuilder: (context, i) => (i == 0) + ? const SizedBox() + : ListTile( + leading: GestureDetector( + onTap: () async { + final icon = + await FlutterIconPicker.showIconPicker( + context, + ); + if (icon == null) return; + categories[i].icon = icon; + setState(() {}); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context) + .colorScheme + .secondary, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + categories[i].icon, + color: Theme.of(context) + .colorScheme + .onSecondary, ), ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - AppLocalizations.of(context).cancel, - ), - ), - ], - title: AppLocalizations.of(context) - .setupCategoriesEditingName, - content: SizedBox( - width: 400, - child: - PlatformField(controller: controller), ), ), - ); - }, - child: Text( - categories[i].name, - style: - const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ), + trailing: IconButton( + icon: const Icon(Icons.cancel), + onPressed: () { + categories.removeAt(i); + setState(() {}); + }, + ), + title: GestureDetector( + onTap: () { + final controller = TextEditingController( + text: categories[i].name, + ); + showDialog( + context: context, + builder: (c) => PlatformDialog( + actions: [ + TextButton( + onPressed: () { + if (controller.text.isEmpty) + return; + categories[i].name = + controller.text; + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context).ok, + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + AppLocalizations.of(context) + .cancel, + ), + ), + ], + title: AppLocalizations.of(context) + .setupCategoriesEditingName, + content: SizedBox( + width: 400, + child: PlatformField( + controller: controller), + ), + ), + ); + }, + child: Text( + categories[i].name, + style: const TextStyle( + fontWeight: FontWeight.bold), + ), + ), + ), itemCount: categories.length, ), ), IconButton( onPressed: () { - var id = 1; + var id = 0; while (categories .where((element) => element.id == id) .isNotEmpty) { @@ -362,7 +374,6 @@ class _SetupViewState extends State { WalletCategory( name: AppLocalizations.of(context) .setupWalletNamePlaceholder, - type: EntryType.expense, id: id, icon: IconData( Icons.question_mark.codePoint,