diff --git a/CHANGELOG.md b/CHANGELOG.md index e5eaffb..6be7ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ -# newVersion +# 2.0.0 - Upgrade dependencies - Use less `await`s in WalletManager class +- Added debt management +- Changed android app ID # 1.1.1 - Removed deprecated code diff --git a/lib/api/debt_entry.dart b/lib/api/debt_entry.dart index b25f28b..7a173bd 100644 --- a/lib/api/debt_entry.dart +++ b/lib/api/debt_entry.dart @@ -37,6 +37,8 @@ class DebtEntry { String name; /// List of people who payed - @JsonKey(defaultValue: DebtPerson.unknownPerson) - List whoPayed; + @JsonKey(defaultValue: _defaultDebtPayers) + final List whoPayed; } + +List _defaultDebtPayers() => [DebtPerson.unknownPerson()]; diff --git a/lib/api/debt_entry.g.dart b/lib/api/debt_entry.g.dart index e939cf1..deb8f4c 100644 --- a/lib/api/debt_entry.g.dart +++ b/lib/api/debt_entry.g.dart @@ -19,7 +19,7 @@ DebtEntry _$DebtEntryFromJson(Map json) { whoPayed: (json['whoPayed'] as List?) ?.map((e) => DebtPerson.fromJson(e as Map)) .toList() ?? - DebtPerson.unknownPerson(), + _defaultDebtPayers(), ); } diff --git a/lib/api/debt_person.dart b/lib/api/debt_person.dart index d17676f..2678588 100644 --- a/lib/api/debt_person.dart +++ b/lib/api/debt_person.dart @@ -6,7 +6,7 @@ part 'debt_person.g.dart'; /// Represents a single person in a debt scenario class DebtPerson { /// Represents a single person in a debt scenario - DebtPerson({required this.id, required this.name}); + DebtPerson({required this.id, required this.name, this.bankAccount}); /// Default [DebtPerson] instance for json_serializable factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown"); @@ -25,4 +25,9 @@ class DebtPerson { /// Identifier that the user will see @JsonKey(defaultValue: "Unknown") String name; + + /// Person's bank account + /// + /// Used to generate a QR code payment + String? bankAccount; } diff --git a/lib/api/debt_person.g.dart b/lib/api/debt_person.g.dart index e20c32b..d7c27b0 100644 --- a/lib/api/debt_person.g.dart +++ b/lib/api/debt_person.g.dart @@ -15,6 +15,7 @@ DebtPerson _$DebtPersonFromJson(Map json) { return DebtPerson( id: (json['id'] as num).toInt(), name: json['name'] as String? ?? 'Unknown', + bankAccount: json['bankAccount'] as String?, ); } @@ -22,4 +23,5 @@ Map _$DebtPersonToJson(DebtPerson instance) => { 'id': instance.id, 'name': instance.name, + 'bankAccount': instance.bankAccount, }; diff --git a/lib/api/debt_scenario.dart b/lib/api/debt_scenario.dart index e3d7120..d07e64c 100644 --- a/lib/api/debt_scenario.dart +++ b/lib/api/debt_scenario.dart @@ -11,8 +11,8 @@ class DebtScenario { required this.id, required this.name, required this.isArchived, + required this.people, this.entries = const [], - this.people = const [], }); /// Generates a class instance from a Map @@ -22,7 +22,7 @@ class DebtScenario { /// Converts the data in this instance into a Map Map toJson() => _$DebtScenarioToJson(this); - /// Unique identified + /// Unique identifier @JsonKey(disallowNullValue: true) final int id; @@ -36,11 +36,29 @@ class DebtScenario { /// All entries @JsonKey(defaultValue: []) - List entries; + final List entries; /// All people @JsonKey(defaultValue: _defaultPeopleList) - List people; + final List people; + + /// Getter for the next unused unique number ID for a [DebtPerson] + int get nextPersonId { + var id = 1; + while (people.where((element) => element.id == id).isNotEmpty) { + id++; // create unique ID + } + return id; + } + + /// Getter for the next unused unique number ID for a [DebtEntry] + int get nextEntryId { + var id = 1; + while (entries.where((element) => element.id == id).isNotEmpty) { + id++; // create unique ID + } + return id; + } } List _defaultPeopleList() => [DebtPerson.unknownPerson()]; diff --git a/lib/api/wallet.dart b/lib/api/wallet.dart index b8cd91b..480dc4a 100644 --- a/lib/api/wallet.dart +++ b/lib/api/wallet.dart @@ -9,6 +9,7 @@ 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/debt_scenario.dart'; import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/wallet_entry.dart'; @@ -31,6 +32,7 @@ class Wallet { this.categories = const [], this.entries = const [], this.recurringEntries = const [], + this.debts = const [], this.starterBalance = 0, }); @@ -53,6 +55,10 @@ class Wallet { @JsonKey(defaultValue: []) final List entries; + /// List of user's [DebtScenario]s + @JsonKey(defaultValue: []) + final List debts; + /// The starting balance of the wallet /// /// Used to calculate current balance @@ -88,6 +94,16 @@ class Wallet { return id; } + /// Getter for the next unused unique number ID in the wallet's **debts** + /// list + int get nextDebtId { + var id = 0; + while (debts.where((element) => element.id == id).isNotEmpty) { + id++; // create unique ID + } + return id; + } + /// Handles adding recurring entries to the entry list void recurEntries() { final n = DateTime.now(); diff --git a/lib/api/wallet.g.dart b/lib/api/wallet.g.dart index 277d410..f02a5a1 100644 --- a/lib/api/wallet.g.dart +++ b/lib/api/wallet.g.dart @@ -25,6 +25,10 @@ Wallet _$WalletFromJson(Map json) => Wallet( RecurringWalletEntry.fromJson(e as Map)) .toList() ?? [], + debts: (json['debts'] as List?) + ?.map((e) => DebtScenario.fromJson(e as Map)) + .toList() ?? + [], starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0, ); @@ -33,6 +37,7 @@ Map _$WalletToJson(Wallet instance) => { 'name': instance.name, 'categories': instance.categories, 'entries': instance.entries, + 'debts': instance.debts, 'starterBalance': instance.starterBalance, 'currency': instance.currency, }; diff --git a/lib/api/wallet_manager.dart b/lib/api/wallet_manager.dart index e9d39c7..80b2d7a 100644 --- a/lib/api/wallet_manager.dart +++ b/lib/api/wallet_manager.dart @@ -16,7 +16,7 @@ import 'package:prasule/main.dart'; /// Used for [Wallet]-managing operations class WalletManager { /// Currently selected wallet - static Wallet? selectedWallet; + static late Wallet selectedWallet; /// Path to the directory with wallet files /// diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 83b55c2..e8eb913 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -122,5 +122,14 @@ "incomePerYearCategory":"Příjmy podle kategorie za rok {year}", "incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}", "selectYear":"Zvolte rok", - "selectMonth":"Zvolte měsíc a rok" + "selectMonth":"Zvolte měsíc a rok", + "debts":"Dlužníček", + "debtNamePlaceholder":"Dluhy přátel", + "people":"Lidé", + "addSomePeople":"Přidej lidi pomocí tlačítka níže", + "bankAccount":"Číslo bankovního účtu", + "noDebtScenarios":"Žádné seznamy dlužníků :(", + "noDebtScenariosSub":"Nový můžete vytvořit pomocí plovoucího tlačítka.", + "noPersonError":"Musíte vložit alespoň jednoho člověka!", + "unnamed":"Bez jména" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9d49ef4..12e403a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -313,5 +313,14 @@ } }, "selectYear":"Select a year", - "selectMonth":"Select a month and year" + "selectMonth":"Select a month and year", + "debts":"Debts", + "debtNamePlaceholder":"Friends' debts", + "people":"People", + "addSomePeople":"Add people using the button below", + "bankAccount":"Bank account number", + "noDebtScenarios":"No debt scenarios :(", + "noDebtScenariosSub":"Create one using the floating action button.", + "noPersonError":"You need to add at least one person!", + "unnamed":"Unnamed" } \ No newline at end of file diff --git a/lib/pw/platformfield.dart b/lib/pw/platformfield.dart index 03781d5..539f994 100644 --- a/lib/pw/platformfield.dart +++ b/lib/pw/platformfield.dart @@ -10,26 +10,26 @@ import 'package:flutter/services.dart'; import 'package:prasule/pw/platformwidget.dart'; /// A [PlatformWidget] implementation of a text field -class PlatformField extends PlatformWidget { - const PlatformField({ - super.key, - this.controller, - this.enabled, - this.labelText, - this.obscureText = false, - this.autocorrect = false, - this.keyboardType, - this.inputFormatters = const [], - this.onChanged, - this.autofillHints, - this.textStyle, - this.textAlign = TextAlign.start, - this.maxLines = 1, - this.focusNode, - this.inputBorder = const OutlineInputBorder(), - this.suffix, - this.prefix, - }); +class PlatformField extends PlatformWidget { + const PlatformField( + {super.key, + this.controller, + this.enabled, + this.labelText, + this.obscureText = false, + this.autocorrect = false, + this.keyboardType, + this.inputFormatters = const [], + this.onChanged, + this.autofillHints, + this.textStyle, + this.textAlign = TextAlign.start, + this.maxLines = 1, + this.focusNode, + this.inputBorder = const OutlineInputBorder(), + this.suffix, + this.prefix, + this.validator}); final TextEditingController? controller; final bool? enabled; final bool obscureText; @@ -46,9 +46,10 @@ class PlatformField extends PlatformWidget { final FocusNode? focusNode; final Widget? suffix; final Widget? prefix; + final String? Function(String?)? validator; @override - TextField createAndroidWidget(BuildContext context) => TextField( + TextFormField createAndroidWidget(BuildContext context) => TextFormField( textAlign: textAlign, controller: controller, enabled: enabled, @@ -67,6 +68,7 @@ class PlatformField extends PlatformWidget { onChanged: onChanged, autofillHints: autofillHints, maxLines: maxLines, + validator: validator, ); @override diff --git a/lib/util/drawer.dart b/lib/util/drawer.dart index e7a4068..e599550 100644 --- a/lib/util/drawer.dart +++ b/lib/util/drawer.dart @@ -5,12 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/views/debts/debt_view.dart'; import 'package:prasule/views/graphs/graph_view.dart'; import 'package:prasule/views/home.dart'; import 'package:prasule/views/recurring/recurring_view.dart'; /// Makes the drawer because I won't enter the same code in every view -Drawer makeDrawer(BuildContext context, int page) => Drawer( +Drawer makeDrawer(BuildContext context, Pages page) => Drawer( child: ListView( children: [ const DrawerHeader(child: Text("Prašule")), @@ -19,9 +20,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( title: Text( AppLocalizations.of(context).home, ), - selected: page == 1, + selected: page == Pages.home, onTap: () { - if (page == 1) { + if (page == Pages.home) { Navigator.of(context).pop(); return; } @@ -34,9 +35,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( title: Text( AppLocalizations.of(context).graphs, ), - selected: page == 2, + selected: page == Pages.graphs, onTap: () { - if (page == 2) { + if (page == Pages.graphs) { Navigator.of(context).pop(); return; } @@ -49,9 +50,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( title: Text( AppLocalizations.of(context).recurringPayments, ), - selected: page == 3, + selected: page == Pages.recurringEntries, onTap: () { - if (page == 3) { + if (page == Pages.recurringEntries) { Navigator.of(context).pop(); return; } @@ -60,6 +61,37 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( ); }, ), + ListTile( + leading: const Icon(Icons.people), + title: Text( + AppLocalizations.of(context).debts, + ), + selected: page == Pages.debts, + onTap: () { + if (page == Pages.debts) { + Navigator.of(context).pop(); + return; + } + Navigator.of(context).pushReplacement( + platformRoute((p0) => const DebtView()), + ); + }, + ), ], ), ); + +/// All the pages that drawer can navigate to +enum Pages { + /// [HomeView] + home, + + /// [GraphView] + graphs, + + /// [RecurringEntriesView] + recurringEntries, + + /// [DebtView] + debts +} diff --git a/lib/views/debts/debt_view.dart b/lib/views/debts/debt_view.dart new file mode 100644 index 0000000..5ba2da0 --- /dev/null +++ b/lib/views/debts/debt_view.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:prasule/api/debt_scenario.dart'; +import 'package:prasule/api/wallet.dart'; +import 'package:prasule/api/wallet_manager.dart'; +import 'package:prasule/main.dart'; +import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/util/drawer.dart'; +import 'package:prasule/views/debts/setup_debt_scenario.dart'; +import 'package:prasule/views/setup.dart'; + +/// Shows the selected [DebtScenario] +class DebtView extends StatefulWidget { + /// Shows the selected [DebtScenario] + const DebtView({super.key}); + + @override + State createState() => _DebtViewState(); +} + +class _DebtViewState extends State { + List wallets = []; + void loadWallet() { + wallets = WalletManager.listWallets(); + if (wallets.isEmpty && mounted) { + unawaited( + Navigator.of(context) + .pushReplacement(platformRoute((c) => const SetupView())), + ); + return; + } + setState(() {}); + } + + @override + void initState() { + super.initState(); + loadWallet(); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: WalletManager.selectedWallet.debts.isEmpty + ? 1 + : WalletManager.selectedWallet.debts.length, + child: Scaffold( + floatingActionButton: FloatingActionButton( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, + child: const Icon(Icons.add), + onPressed: () { + Navigator.of(context) + .push(platformRoute((c) => const SetupDebtScenario())) + .then((v) { + setState(() {}); + }); + }, + ), + appBar: AppBar( + title: DropdownButton( + value: wallets.indexOf( + wallets + .where((w) => w.name == WalletManager.selectedWallet.name) + .first, + ), + 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) { + await Navigator.of(context).push( + platformRoute( + (c) => const SetupView( + newWallet: true, + ), + ), + ); + wallets = WalletManager.listWallets(); + logger.i(wallets.length); + WalletManager.selectedWallet = wallets.last; + setState(() {}); + return; + } + WalletManager.selectedWallet = wallets[v]; + setState(() {}); + }, + ), + bottom: TabBar( + tabs: (WalletManager.selectedWallet.debts.isEmpty) + ? [Text(AppLocalizations.of(context).welcome)] + : List.generate( + WalletManager.selectedWallet.debts.length, + (index) => Tab( + text: WalletManager.selectedWallet.debts[index].name, + ), + ), + ), + ), + drawer: makeDrawer(context, Pages.debts), + body: TabBarView( + children: (WalletManager.selectedWallet.debts.isEmpty) + ? [ + Column( + children: [ + const SizedBox( + height: 20, + ), + Text( + AppLocalizations.of(context).noDebtScenarios, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + ), + ), + Text(AppLocalizations.of(context).noDebtScenariosSub), + ], + ), + ] + : List.generate( + WalletManager.selectedWallet.debts.length, + (c) => const Placeholder(), + ), + ), + ), + ); + } +} diff --git a/lib/views/debts/setup_debt_scenario.dart b/lib/views/debts/setup_debt_scenario.dart new file mode 100644 index 0000000..1a5ee06 --- /dev/null +++ b/lib/views/debts/setup_debt_scenario.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:prasule/api/debt_person.dart'; +import 'package:prasule/api/debt_scenario.dart'; +import 'package:prasule/api/wallet_manager.dart'; +import 'package:prasule/pw/platformfield.dart'; +import 'package:prasule/util/show_message.dart'; + +/// Used to create and/or edit [DebtScenario]s +class SetupDebtScenario extends StatefulWidget { + /// Used to create and/or edit [DebtScenario]s + const SetupDebtScenario({super.key, this.toEdit}); + + /// If not null, loads this [DebtScenario]'s data for editing + final DebtScenario? toEdit; + + @override + State createState() => _SetupDebtScenarioState(); +} + +class _SetupDebtScenarioState extends State { + late DebtScenario _scenario; + final _nameController = TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + + /// Stores data for each [ExpansionPanel] + final List _isOpen = []; + final List _panelControllers = []; + bool _isLoading = true; + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_isLoading) return; + _scenario = widget.toEdit ?? + DebtScenario( + id: WalletManager.selectedWallet.nextDebtId, + name: AppLocalizations.of(context).debtNamePlaceholder, + isArchived: false, + people: [], + entries: [], + ); + + // Load stuff from the scenario + _nameController.text = _scenario.name; + _isOpen.addAll(List.filled(_scenario.people.length, false)); + _panelControllers.addAll( + List.filled(_scenario.people.length * 2, TextEditingController()), + ); + + _isLoading = false; + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: SingleChildScrollView( + child: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.95, + child: Form( + key: _formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + child: PlatformField( + labelText: AppLocalizations.of(context).name, + controller: _nameController, + validator: (input) { + if (input == null || input.isEmpty) { + return AppLocalizations.of(context).errorEmptyName; + } + return null; + }, + ), + ), + ), + const SizedBox( + height: 15, + ), + Text(AppLocalizations.of(context).people), + const SizedBox( + height: 10, + ), + LimitedBox( + maxHeight: MediaQuery.of(context).size.height * 0.6, + child: Container( + width: MediaQuery.of(context).size.width * 0.8, + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: (_scenario.people.isEmpty) + ? Text(AppLocalizations.of(context).addSomePeople) + : ExpansionPanelList( + expansionCallback: (panelIndex, isExpanded) { + _isOpen[panelIndex] = isExpanded; + setState(() {}); + }, + children: List.generate( + _scenario.people.length, + (index) => ExpansionPanel( + isExpanded: _isOpen[index], + headerBuilder: (context, isOpen) => Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(4), + child: SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.45, + child: PlatformField( + labelText: + AppLocalizations.of(context) + .name, + controller: + _panelControllers[index], + onChanged: (input) { + _scenario.people[index].name = + input; + setState(() {}); + }, + validator: (input) { + if (input == null || + input.isEmpty) { + return AppLocalizations.of( + context, + ).errorEmptyName; + } + return null; + }, + ), + ), + ), + IconButton( + onPressed: () { + _scenario.people.removeAt(index); + _panelControllers.removeRange( + index, + index + 2, + ); + _isOpen.removeAt(index); + setState(() {}); + }, + icon: const Icon( + Icons.delete_forever, + ), + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: MediaQuery.of(context) + .size + .width * + 0.8, + child: PlatformField( + labelText: + AppLocalizations.of(context) + .bankAccount, + controller: _panelControllers[ + index + 1], + onChanged: (input) { + _scenario.people[index] + .bankAccount = + (input.isEmpty) + ? null + : input; + setState(() {}); + }, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 10, + ), + IconButton( + onPressed: () { + _scenario.people.add( + DebtPerson(id: _scenario.nextPersonId, name: ""), + ); + _isOpen.add(false); + _panelControllers.addAll( + [TextEditingController(), TextEditingController()], + ); + setState(() {}); + }, + icon: const Icon(Icons.add), + ), + const SizedBox( + height: 15, + ), + TextButton( + onPressed: () { + if (!_formKey.currentState!.validate()) { + return; + } + if (_scenario.people.isEmpty) { + showMessage( + AppLocalizations.of(context).noPersonError, + context, + ); + return; + } + _scenario.name = _nameController.text; + if (widget.toEdit != null) { + // If editing, only replace + WalletManager.selectedWallet.debts[WalletManager + .selectedWallet.debts + .indexWhere((d) => d.id == widget.toEdit!.id)] = + _scenario; + } else { + // else add new + WalletManager.selectedWallet.debts.add(_scenario); + } + WalletManager.saveWallet(WalletManager.selectedWallet); + Navigator.of(context).pop(); + }, + child: Text(AppLocalizations.of(context).save), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/views/graphs/graph_view.dart b/lib/views/graphs/graph_view.dart index c09684e..ae6de55 100644 --- a/lib/views/graphs/graph_view.dart +++ b/lib/views/graphs/graph_view.dart @@ -31,7 +31,6 @@ class GraphView extends StatefulWidget { class _GraphViewState extends State { var _selectedDate = DateTime.now(); - Wallet? selectedWallet; List wallets = []; String? locale; bool yearly = true; @@ -48,9 +47,8 @@ class _GraphViewState extends State { yearly ? 12 : DateTime(d.year, d.month, 0).day, 0, ); - if (selectedWallet == null) return []; for (var i = 0; i < data.length; i++) { - final entriesForRange = selectedWallet!.entries.where( + final entriesForRange = WalletManager.selectedWallet.entries.where( (element) => ((!yearly) ? element.date.month == _selectedDate.month && @@ -80,9 +78,9 @@ class _GraphViewState extends State { ); return; } - selectedWallet = wallets.first; + WalletManager.selectedWallet = wallets.first; availableYears.clear(); - for (final entry in selectedWallet!.entries) { + for (final entry in WalletManager.selectedWallet.entries) { if (!availableYears.any((element) => element.value == entry.date.year)) { availableYears.add( WheelChoice( @@ -200,11 +198,11 @@ class _GraphViewState extends State { ], ), title: DropdownButton( - value: (selectedWallet == null) - ? -1 - : wallets.indexOf( - wallets.where((w) => w.name == selectedWallet!.name).first, - ), + value: wallets.indexOf( + wallets + .where((w) => w.name == WalletManager.selectedWallet.name) + .first, + ), items: [ ...wallets.map( (e) => DropdownMenuItem( @@ -230,11 +228,11 @@ class _GraphViewState extends State { ); wallets = WalletManager.listWallets(); logger.i(wallets.length); - selectedWallet = wallets.last; + WalletManager.selectedWallet = wallets.last; setState(() {}); return; } - selectedWallet = wallets[v]; + WalletManager.selectedWallet = wallets[v]; setState(() {}); }, ), @@ -253,8 +251,8 @@ class _GraphViewState extends State { ), ) .then((value) async { - selectedWallet = - WalletManager.loadWallet(selectedWallet!.name); + WalletManager.selectedWallet = WalletManager.loadWallet( + WalletManager.selectedWallet.name); final s = await SharedPreferences.getInstance(); chartType = s.getInt("monthlygraph") ?? 2; setState(() {}); @@ -266,453 +264,420 @@ class _GraphViewState extends State { ), ], ), - drawer: makeDrawer(context, 2), + drawer: makeDrawer(context, Pages.graphs), body: TabBarView( children: [ // EXPENSE TAB SingleChildScrollView( child: Center( - child: (selectedWallet == null) - ? const CircularProgressIndicator( - strokeWidth: 5, - ) - : SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: 200, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).monthly, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Switch.adaptive( - value: yearly, - onChanged: (v) async { - yearly = v; - final s = - await SharedPreferences.getInstance(); - chartType = yearly - ? (s.getInt("yearlygraph") ?? 1) - : (s.getInt("monthlygraph") ?? 2); + Text( + AppLocalizations.of(context).monthly, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Switch.adaptive( + value: yearly, + onChanged: (v) async { + yearly = v; + final s = await SharedPreferences.getInstance(); + chartType = yearly + ? (s.getInt("yearlygraph") ?? 1) + : (s.getInt("monthlygraph") ?? 2); - setState(() {}); - }, - ), - Text( - AppLocalizations.of(context).yearly, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), + setState(() {}); + }, ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: (MediaQuery.of(context) - .platformBrightness == - Brightness.light) - ? [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 3, - blurRadius: 7, - offset: const Offset( - 0, - 3, - ), - ), - ] - : null, - color: (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.surface, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Text( - yearly - ? AppLocalizations.of(context) - .expensesPerYear( - _selectedDate.year, - ) - : AppLocalizations.of(context) - .expensesPerMonth( - DateFormat.yMMMM(locale) - .format(_selectedDate), - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - height: 15, - ), - SizedBox( - width: MediaQuery.of(context).size.width * - 0.9, - height: - MediaQuery.of(context).size.height * - 0.35, - child: (chartType == null) - ? const CircularProgressIndicator() - : (chartType == 1) - ? ExpensesBarChart( - currency: - selectedWallet!.currency, - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: - generateChartData( - EntryType.expense, - ), - incomeData: const [], - ) - : Padding( - padding: - const EdgeInsets.all(8), - child: ExpensesLineChart( - currency: selectedWallet! - .currency, - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: - generateChartData( - EntryType.expense, - ), - incomeData: const [], - ), - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: (MediaQuery.of(context) - .platformBrightness == - Brightness.light) - ? [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 3, - blurRadius: 7, - offset: const Offset( - 0, - 3, - ), - ), - ] - : null, - color: (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.surface, - ), - width: MediaQuery.of(context).size.width * 0.95, - height: MediaQuery.of(context).size.height * 0.4, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Flexible( - child: Text( - textAlign: TextAlign.center, - yearly - ? AppLocalizations.of(context) - .expensesPerYearCategory( - _selectedDate.year, - ) - : AppLocalizations.of(context) - .expensesPerMonthCategory( - DateFormat.yMMMM(locale) - .format(_selectedDate), - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6), - child: CategoriesPieChart( - // TODO: better size adaptivity without overflow - locale: locale ?? "en", - symbol: selectedWallet!.currency.symbol, - entries: selectedWallet!.entries - .where( - (element) => - ((!yearly) - ? element.date.month == - _selectedDate - .month && - element.date.year == - _selectedDate.year - : element.date.year == - _selectedDate.year) && - element.type == - EntryType.expense, - ) - .toList(), - categories: selectedWallet!.categories, - ), - ), - ], + Text( + AppLocalizations.of(context).yearly, + style: const TextStyle( + fontWeight: FontWeight.bold, ), ), ], ), ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: + (MediaQuery.of(context).platformBrightness == + Brightness.light) + ? [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 3, + blurRadius: 7, + offset: const Offset( + 0, + 3, + ), + ), + ] + : null, + color: (MediaQuery.of(context).platformBrightness == + Brightness.dark) + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + yearly + ? AppLocalizations.of(context) + .expensesPerYear( + _selectedDate.year, + ) + : AppLocalizations.of(context) + .expensesPerMonth( + DateFormat.yMMMM(locale) + .format(_selectedDate), + ), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 15, + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: + MediaQuery.of(context).size.height * 0.35, + child: (chartType == null) + ? const CircularProgressIndicator() + : (chartType == 1) + ? ExpensesBarChart( + currency: WalletManager + .selectedWallet.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: generateChartData( + EntryType.expense, + ), + incomeData: const [], + ) + : Padding( + padding: const EdgeInsets.all(8), + child: ExpensesLineChart( + currency: WalletManager + .selectedWallet.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: generateChartData( + EntryType.expense, + ), + incomeData: const [], + ), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: + (MediaQuery.of(context).platformBrightness == + Brightness.light) + ? [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 3, + blurRadius: 7, + offset: const Offset( + 0, + 3, + ), + ), + ] + : null, + color: (MediaQuery.of(context).platformBrightness == + Brightness.dark) + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + ), + width: MediaQuery.of(context).size.width * 0.95, + height: MediaQuery.of(context).size.height * 0.4, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Flexible( + child: Text( + textAlign: TextAlign.center, + yearly + ? AppLocalizations.of(context) + .expensesPerYearCategory( + _selectedDate.year, + ) + : AppLocalizations.of(context) + .expensesPerMonthCategory( + DateFormat.yMMMM(locale) + .format(_selectedDate), + ), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(6), + child: CategoriesPieChart( + // TODO: better size adaptivity without overflow + locale: locale ?? "en", + symbol: WalletManager + .selectedWallet.currency.symbol, + entries: WalletManager.selectedWallet.entries + .where( + (element) => + ((!yearly) + ? element.date.month == + _selectedDate.month && + element.date.year == + _selectedDate.year + : element.date.year == + _selectedDate.year) && + element.type == EntryType.expense, + ) + .toList(), + categories: + WalletManager.selectedWallet.categories, + ), + ), + ], + ), + ), + ], + ), + ), ), ), // Expense Tab END SingleChildScrollView( child: Center( - child: (selectedWallet == null) - ? const CircularProgressIndicator( - strokeWidth: 5, - ) - : SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 200, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - SizedBox( - width: 200, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - AppLocalizations.of(context).monthly, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - Switch.adaptive( - value: yearly, - onChanged: (v) async { - yearly = v; - final s = - await SharedPreferences.getInstance(); - chartType = yearly - ? (s.getInt("yearlygraph") ?? 1) - : (s.getInt("monthlygraph") ?? 2); + Text( + AppLocalizations.of(context).monthly, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + Switch.adaptive( + value: yearly, + onChanged: (v) async { + yearly = v; + final s = await SharedPreferences.getInstance(); + chartType = yearly + ? (s.getInt("yearlygraph") ?? 1) + : (s.getInt("monthlygraph") ?? 2); - setState(() {}); - }, - ), - Text( - AppLocalizations.of(context).yearly, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), + setState(() {}); + }, ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: (MediaQuery.of(context) - .platformBrightness == - Brightness.light) - ? [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 3, - blurRadius: 7, - offset: const Offset( - 0, - 3, - ), - ), - ] - : null, - color: (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.surface, - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Text( - yearly - ? AppLocalizations.of(context) - .incomePerYear( - _selectedDate.year, - ) - : AppLocalizations.of(context) - .incomePerMonth( - DateFormat.yMMMM(locale) - .format(_selectedDate), - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox( - height: 15, - ), - SizedBox( - width: MediaQuery.of(context).size.width * - 0.9, - height: - MediaQuery.of(context).size.height * - 0.35, - child: (chartType == null) - ? const CircularProgressIndicator() - : (chartType == 1) - ? ExpensesBarChart( - currency: - selectedWallet!.currency, - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: const [], - incomeData: generateChartData( - EntryType.income, - ), - ) - : Padding( - padding: - const EdgeInsets.all(8), - child: ExpensesLineChart( - currency: selectedWallet! - .currency, - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: const [], - incomeData: - generateChartData( - EntryType.income, - ), - ), - ), - ), - ], - ), - ), - ), - const SizedBox( - height: 25, - ), - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - boxShadow: (MediaQuery.of(context) - .platformBrightness == - Brightness.light) - ? [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 3, - blurRadius: 7, - offset: const Offset( - 0, - 3, - ), - ), - ] - : null, - color: (MediaQuery.of(context) - .platformBrightness == - Brightness.dark) - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.surface, - ), - width: MediaQuery.of(context).size.width * 0.95, - height: MediaQuery.of(context).size.height * 0.4, - child: Column( - children: [ - const SizedBox( - height: 10, - ), - Flexible( - child: Text( - yearly - ? AppLocalizations.of(context) - .incomePerYearCategory( - _selectedDate.year, - ) - : AppLocalizations.of(context) - .incomePerMonthCategory( - DateFormat.yMMMM(locale) - .format(_selectedDate), - ), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(6), - child: CategoriesPieChart( - locale: locale ?? "en", - symbol: selectedWallet!.currency.symbol, - entries: selectedWallet!.entries - .where( - (element) => - ((!yearly) - ? element.date.month == - _selectedDate - .month && - element.date.year == - _selectedDate.year - : element.date.year == - _selectedDate.year) && - element.type == - EntryType.income, - ) - .toList(), - categories: selectedWallet!.categories, - ), - ), - ], + Text( + AppLocalizations.of(context).yearly, + style: const TextStyle( + fontWeight: FontWeight.bold, ), ), ], ), ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: + (MediaQuery.of(context).platformBrightness == + Brightness.light) + ? [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 3, + blurRadius: 7, + offset: const Offset( + 0, + 3, + ), + ), + ] + : null, + color: (MediaQuery.of(context).platformBrightness == + Brightness.dark) + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + yearly + ? AppLocalizations.of(context) + .incomePerYear( + _selectedDate.year, + ) + : AppLocalizations.of(context) + .incomePerMonth( + DateFormat.yMMMM(locale) + .format(_selectedDate), + ), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox( + height: 15, + ), + SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: + MediaQuery.of(context).size.height * 0.35, + child: (chartType == null) + ? const CircularProgressIndicator() + : (chartType == 1) + ? ExpensesBarChart( + currency: WalletManager + .selectedWallet.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: const [], + incomeData: generateChartData( + EntryType.income, + ), + ) + : Padding( + padding: const EdgeInsets.all(8), + child: ExpensesLineChart( + currency: WalletManager + .selectedWallet.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: const [], + incomeData: generateChartData( + EntryType.income, + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 25, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: + (MediaQuery.of(context).platformBrightness == + Brightness.light) + ? [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 3, + blurRadius: 7, + offset: const Offset( + 0, + 3, + ), + ), + ] + : null, + color: (MediaQuery.of(context).platformBrightness == + Brightness.dark) + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + ), + width: MediaQuery.of(context).size.width * 0.95, + height: MediaQuery.of(context).size.height * 0.4, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Flexible( + child: Text( + yearly + ? AppLocalizations.of(context) + .incomePerYearCategory( + _selectedDate.year, + ) + : AppLocalizations.of(context) + .incomePerMonthCategory( + DateFormat.yMMMM(locale) + .format(_selectedDate), + ), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(6), + child: CategoriesPieChart( + locale: locale ?? "en", + symbol: WalletManager + .selectedWallet.currency.symbol, + entries: WalletManager.selectedWallet.entries + .where( + (element) => + ((!yearly) + ? element.date.month == + _selectedDate.month && + element.date.year == + _selectedDate.year + : element.date.year == + _selectedDate.year) && + element.type == EntryType.income, + ) + .toList(), + categories: + WalletManager.selectedWallet.categories, + ), + ), + ], + ), + ), + ], + ), + ), ), ), // Income Tab END ], diff --git a/lib/views/home.dart b/lib/views/home.dart index 9674447..e6f2498 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -94,8 +94,10 @@ class _HomeViewState extends State { setState(() {}); }, child: Scaffold( - drawer: makeDrawer(context, 1), + drawer: makeDrawer(context, Pages.home), floatingActionButton: SpeedDial( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of(context).colorScheme.onPrimary, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16)), ), diff --git a/lib/views/initialization_screen.dart b/lib/views/initialization_screen.dart index dc186a5..5c1a197 100644 --- a/lib/views/initialization_screen.dart +++ b/lib/views/initialization_screen.dart @@ -31,6 +31,7 @@ class _InitializationScreenState extends State { .pushReplacement(platformRoute((c) => const SetupView())); return; } + WalletManager.selectedWallet = wallets.first; Navigator.of(context) .pushReplacement(platformRoute((c) => const HomeView())); }); diff --git a/lib/views/recurring/recurring_view.dart b/lib/views/recurring/recurring_view.dart index e45cbfb..1fc4094 100644 --- a/lib/views/recurring/recurring_view.dart +++ b/lib/views/recurring/recurring_view.dart @@ -64,7 +64,7 @@ class _RecurringEntriesViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - drawer: makeDrawer(context, 3), + drawer: makeDrawer(context, Pages.recurringEntries), appBar: AppBar( title: DropdownButton( value: (selectedWallet == null) diff --git a/lib/views/setup.dart b/lib/views/setup.dart index e7a739a..7ee4513 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -167,6 +167,9 @@ class _SetupViewState extends State { Navigator.of(context).pop(); return; } + + WalletManager.selectedWallet = wallet; + if (!context.mounted) return; unawaited( Navigator.of(context).pushReplacement(