feat: creating debt scenarios

also changed way of loading selected wallet
This commit is contained in:
Matyáš Caras 2024-07-19 17:34:45 +02:00
parent 300f359070
commit ef52caa836
Signed by: hernik
GPG key ID: 2A3175F98820C5C6
20 changed files with 946 additions and 478 deletions

View file

@ -1,6 +1,8 @@
# newVersion # 2.0.0
- Upgrade dependencies - Upgrade dependencies
- Use less `await`s in WalletManager class - Use less `await`s in WalletManager class
- Added debt management
- Changed android app ID
# 1.1.1 # 1.1.1
- Removed deprecated code - Removed deprecated code

View file

@ -37,6 +37,8 @@ class DebtEntry {
String name; String name;
/// List of people who payed /// List of people who payed
@JsonKey(defaultValue: DebtPerson.unknownPerson) @JsonKey(defaultValue: _defaultDebtPayers)
List<DebtPerson> whoPayed; final List<DebtPerson> whoPayed;
} }
List<DebtPerson> _defaultDebtPayers() => [DebtPerson.unknownPerson()];

View file

@ -19,7 +19,7 @@ DebtEntry _$DebtEntryFromJson(Map<String, dynamic> json) {
whoPayed: (json['whoPayed'] as List<dynamic>?) whoPayed: (json['whoPayed'] as List<dynamic>?)
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>)) ?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
DebtPerson.unknownPerson(), _defaultDebtPayers(),
); );
} }

View file

@ -6,7 +6,7 @@ part 'debt_person.g.dart';
/// Represents a single person in a debt scenario /// Represents a single person in a debt scenario
class DebtPerson { class DebtPerson {
/// Represents a single person in a debt scenario /// 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 /// Default [DebtPerson] instance for json_serializable
factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown"); factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown");
@ -25,4 +25,9 @@ class DebtPerson {
/// Identifier that the user will see /// Identifier that the user will see
@JsonKey(defaultValue: "Unknown") @JsonKey(defaultValue: "Unknown")
String name; String name;
/// Person's bank account
///
/// Used to generate a QR code payment
String? bankAccount;
} }

View file

@ -15,6 +15,7 @@ DebtPerson _$DebtPersonFromJson(Map<String, dynamic> json) {
return DebtPerson( return DebtPerson(
id: (json['id'] as num).toInt(), id: (json['id'] as num).toInt(),
name: json['name'] as String? ?? 'Unknown', name: json['name'] as String? ?? 'Unknown',
bankAccount: json['bankAccount'] as String?,
); );
} }
@ -22,4 +23,5 @@ Map<String, dynamic> _$DebtPersonToJson(DebtPerson instance) =>
<String, dynamic>{ <String, dynamic>{
'id': instance.id, 'id': instance.id,
'name': instance.name, 'name': instance.name,
'bankAccount': instance.bankAccount,
}; };

View file

@ -11,8 +11,8 @@ class DebtScenario {
required this.id, required this.id,
required this.name, required this.name,
required this.isArchived, required this.isArchived,
required this.people,
this.entries = const [], this.entries = const [],
this.people = const [],
}); });
/// Generates a class instance from a Map /// Generates a class instance from a Map
@ -22,7 +22,7 @@ class DebtScenario {
/// Converts the data in this instance into a Map /// Converts the data in this instance into a Map
Map<String, dynamic> toJson() => _$DebtScenarioToJson(this); Map<String, dynamic> toJson() => _$DebtScenarioToJson(this);
/// Unique identified /// Unique identifier
@JsonKey(disallowNullValue: true) @JsonKey(disallowNullValue: true)
final int id; final int id;
@ -36,11 +36,29 @@ class DebtScenario {
/// All entries /// All entries
@JsonKey(defaultValue: []) @JsonKey(defaultValue: [])
List<DebtEntry> entries; final List<DebtEntry> entries;
/// All people /// All people
@JsonKey(defaultValue: _defaultPeopleList) @JsonKey(defaultValue: _defaultPeopleList)
List<DebtPerson> people; final List<DebtPerson> 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<DebtPerson> _defaultPeopleList() => [DebtPerson.unknownPerson()]; List<DebtPerson> _defaultPeopleList() => [DebtPerson.unknownPerson()];

View file

@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.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/entry_data.dart';
import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/recurring_entry.dart';
import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/wallet_entry.dart';
@ -31,6 +32,7 @@ class Wallet {
this.categories = const [], this.categories = const [],
this.entries = const [], this.entries = const [],
this.recurringEntries = const [], this.recurringEntries = const [],
this.debts = const [],
this.starterBalance = 0, this.starterBalance = 0,
}); });
@ -53,6 +55,10 @@ class Wallet {
@JsonKey(defaultValue: []) @JsonKey(defaultValue: [])
final List<WalletSingleEntry> entries; final List<WalletSingleEntry> entries;
/// List of user's [DebtScenario]s
@JsonKey(defaultValue: [])
final List<DebtScenario> debts;
/// The starting balance of the wallet /// The starting balance of the wallet
/// ///
/// Used to calculate current balance /// Used to calculate current balance
@ -88,6 +94,16 @@ class Wallet {
return id; 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 /// Handles adding recurring entries to the entry list
void recurEntries() { void recurEntries() {
final n = DateTime.now(); final n = DateTime.now();

View file

@ -25,6 +25,10 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
RecurringWalletEntry.fromJson(e as Map<String, dynamic>)) RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[], [],
debts: (json['debts'] as List<dynamic>?)
?.map((e) => DebtScenario.fromJson(e as Map<String, dynamic>))
.toList() ??
[],
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0, starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
); );
@ -33,6 +37,7 @@ Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
'categories': instance.categories, 'categories': instance.categories,
'entries': instance.entries, 'entries': instance.entries,
'debts': instance.debts,
'starterBalance': instance.starterBalance, 'starterBalance': instance.starterBalance,
'currency': instance.currency, 'currency': instance.currency,
}; };

View file

@ -16,7 +16,7 @@ import 'package:prasule/main.dart';
/// Used for [Wallet]-managing operations /// Used for [Wallet]-managing operations
class WalletManager { class WalletManager {
/// Currently selected wallet /// Currently selected wallet
static Wallet? selectedWallet; static late Wallet selectedWallet;
/// Path to the directory with wallet files /// Path to the directory with wallet files
/// ///

View file

@ -122,5 +122,14 @@
"incomePerYearCategory":"Příjmy podle kategorie za rok {year}", "incomePerYearCategory":"Příjmy podle kategorie za rok {year}",
"incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}", "incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}",
"selectYear":"Zvolte rok", "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"
} }

View file

@ -313,5 +313,14 @@
} }
}, },
"selectYear":"Select a year", "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"
} }

View file

@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
import 'package:prasule/pw/platformwidget.dart'; import 'package:prasule/pw/platformwidget.dart';
/// A [PlatformWidget] implementation of a text field /// A [PlatformWidget] implementation of a text field
class PlatformField extends PlatformWidget<TextField, CupertinoTextField> { class PlatformField extends PlatformWidget<TextFormField, CupertinoTextField> {
const PlatformField({ const PlatformField(
super.key, {super.key,
this.controller, this.controller,
this.enabled, this.enabled,
this.labelText, this.labelText,
@ -29,7 +29,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
this.inputBorder = const OutlineInputBorder(), this.inputBorder = const OutlineInputBorder(),
this.suffix, this.suffix,
this.prefix, this.prefix,
}); this.validator});
final TextEditingController? controller; final TextEditingController? controller;
final bool? enabled; final bool? enabled;
final bool obscureText; final bool obscureText;
@ -46,9 +46,10 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
final FocusNode? focusNode; final FocusNode? focusNode;
final Widget? suffix; final Widget? suffix;
final Widget? prefix; final Widget? prefix;
final String? Function(String?)? validator;
@override @override
TextField createAndroidWidget(BuildContext context) => TextField( TextFormField createAndroidWidget(BuildContext context) => TextFormField(
textAlign: textAlign, textAlign: textAlign,
controller: controller, controller: controller,
enabled: enabled, enabled: enabled,
@ -67,6 +68,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
onChanged: onChanged, onChanged: onChanged,
autofillHints: autofillHints, autofillHints: autofillHints,
maxLines: maxLines, maxLines: maxLines,
validator: validator,
); );
@override @override

View file

@ -5,12 +5,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/pw/platformroute.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/graphs/graph_view.dart';
import 'package:prasule/views/home.dart'; import 'package:prasule/views/home.dart';
import 'package:prasule/views/recurring/recurring_view.dart'; import 'package:prasule/views/recurring/recurring_view.dart';
/// Makes the drawer because I won't enter the same code in every view /// 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( child: ListView(
children: [ children: [
const DrawerHeader(child: Text("Prašule")), const DrawerHeader(child: Text("Prašule")),
@ -19,9 +20,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
title: Text( title: Text(
AppLocalizations.of(context).home, AppLocalizations.of(context).home,
), ),
selected: page == 1, selected: page == Pages.home,
onTap: () { onTap: () {
if (page == 1) { if (page == Pages.home) {
Navigator.of(context).pop(); Navigator.of(context).pop();
return; return;
} }
@ -34,9 +35,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
title: Text( title: Text(
AppLocalizations.of(context).graphs, AppLocalizations.of(context).graphs,
), ),
selected: page == 2, selected: page == Pages.graphs,
onTap: () { onTap: () {
if (page == 2) { if (page == Pages.graphs) {
Navigator.of(context).pop(); Navigator.of(context).pop();
return; return;
} }
@ -49,9 +50,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
title: Text( title: Text(
AppLocalizations.of(context).recurringPayments, AppLocalizations.of(context).recurringPayments,
), ),
selected: page == 3, selected: page == Pages.recurringEntries,
onTap: () { onTap: () {
if (page == 3) { if (page == Pages.recurringEntries) {
Navigator.of(context).pop(); Navigator.of(context).pop();
return; 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
}

View file

@ -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<DebtView> createState() => _DebtViewState();
}
class _DebtViewState extends State<DebtView> {
List<Wallet> 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<int>(
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(),
),
),
),
);
}
}

View file

@ -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<SetupDebtScenario> createState() => _SetupDebtScenarioState();
}
class _SetupDebtScenarioState extends State<SetupDebtScenario> {
late DebtScenario _scenario;
final _nameController = TextEditingController();
final GlobalKey<FormState> _formKey = GlobalKey();
/// Stores data for each [ExpansionPanel]
final List<bool> _isOpen = [];
final List<TextEditingController> _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<ExpansionPanel>.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),
),
],
),
),
),
),
),
);
}
}

View file

@ -31,7 +31,6 @@ class GraphView extends StatefulWidget {
class _GraphViewState extends State<GraphView> { class _GraphViewState extends State<GraphView> {
var _selectedDate = DateTime.now(); var _selectedDate = DateTime.now();
Wallet? selectedWallet;
List<Wallet> wallets = []; List<Wallet> wallets = [];
String? locale; String? locale;
bool yearly = true; bool yearly = true;
@ -48,9 +47,8 @@ class _GraphViewState extends State<GraphView> {
yearly ? 12 : DateTime(d.year, d.month, 0).day, yearly ? 12 : DateTime(d.year, d.month, 0).day,
0, 0,
); );
if (selectedWallet == null) return [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
final entriesForRange = selectedWallet!.entries.where( final entriesForRange = WalletManager.selectedWallet.entries.where(
(element) => (element) =>
((!yearly) ((!yearly)
? element.date.month == _selectedDate.month && ? element.date.month == _selectedDate.month &&
@ -80,9 +78,9 @@ class _GraphViewState extends State<GraphView> {
); );
return; return;
} }
selectedWallet = wallets.first; WalletManager.selectedWallet = wallets.first;
availableYears.clear(); availableYears.clear();
for (final entry in selectedWallet!.entries) { for (final entry in WalletManager.selectedWallet.entries) {
if (!availableYears.any((element) => element.value == entry.date.year)) { if (!availableYears.any((element) => element.value == entry.date.year)) {
availableYears.add( availableYears.add(
WheelChoice<int>( WheelChoice<int>(
@ -200,10 +198,10 @@ class _GraphViewState extends State<GraphView> {
], ],
), ),
title: DropdownButton<int>( title: DropdownButton<int>(
value: (selectedWallet == null) value: wallets.indexOf(
? -1 wallets
: wallets.indexOf( .where((w) => w.name == WalletManager.selectedWallet.name)
wallets.where((w) => w.name == selectedWallet!.name).first, .first,
), ),
items: [ items: [
...wallets.map( ...wallets.map(
@ -230,11 +228,11 @@ class _GraphViewState extends State<GraphView> {
); );
wallets = WalletManager.listWallets(); wallets = WalletManager.listWallets();
logger.i(wallets.length); logger.i(wallets.length);
selectedWallet = wallets.last; WalletManager.selectedWallet = wallets.last;
setState(() {}); setState(() {});
return; return;
} }
selectedWallet = wallets[v]; WalletManager.selectedWallet = wallets[v];
setState(() {}); setState(() {});
}, },
), ),
@ -253,8 +251,8 @@ class _GraphViewState extends State<GraphView> {
), ),
) )
.then((value) async { .then((value) async {
selectedWallet = WalletManager.selectedWallet = WalletManager.loadWallet(
WalletManager.loadWallet(selectedWallet!.name); WalletManager.selectedWallet.name);
final s = await SharedPreferences.getInstance(); final s = await SharedPreferences.getInstance();
chartType = s.getInt("monthlygraph") ?? 2; chartType = s.getInt("monthlygraph") ?? 2;
setState(() {}); setState(() {});
@ -266,17 +264,13 @@ class _GraphViewState extends State<GraphView> {
), ),
], ],
), ),
drawer: makeDrawer(context, 2), drawer: makeDrawer(context, Pages.graphs),
body: TabBarView( body: TabBarView(
children: [ children: [
// EXPENSE TAB // EXPENSE TAB
SingleChildScrollView( SingleChildScrollView(
child: Center( child: Center(
child: (selectedWallet == null) child: SizedBox(
? const CircularProgressIndicator(
strokeWidth: 5,
)
: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: Column( child: Column(
@ -285,8 +279,7 @@ class _GraphViewState extends State<GraphView> {
SizedBox( SizedBox(
width: 200, width: 200,
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
AppLocalizations.of(context).monthly, AppLocalizations.of(context).monthly,
@ -298,8 +291,7 @@ class _GraphViewState extends State<GraphView> {
value: yearly, value: yearly,
onChanged: (v) async { onChanged: (v) async {
yearly = v; yearly = v;
final s = final s = await SharedPreferences.getInstance();
await SharedPreferences.getInstance();
chartType = yearly chartType = yearly
? (s.getInt("yearlygraph") ?? 1) ? (s.getInt("yearlygraph") ?? 1)
: (s.getInt("monthlygraph") ?? 2); : (s.getInt("monthlygraph") ?? 2);
@ -319,8 +311,8 @@ class _GraphViewState extends State<GraphView> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: (MediaQuery.of(context) boxShadow:
.platformBrightness == (MediaQuery.of(context).platformBrightness ==
Brightness.light) Brightness.light)
? [ ? [
BoxShadow( BoxShadow(
@ -334,12 +326,9 @@ class _GraphViewState extends State<GraphView> {
), ),
] ]
: null, : null,
color: (MediaQuery.of(context) color: (MediaQuery.of(context).platformBrightness ==
.platformBrightness ==
Brightness.dark) Brightness.dark)
? Theme.of(context) ? Theme.of(context).colorScheme.secondaryContainer
.colorScheme
.secondaryContainer
: Theme.of(context).colorScheme.surface, : Theme.of(context).colorScheme.surface,
), ),
child: Padding( child: Padding(
@ -366,37 +355,32 @@ class _GraphViewState extends State<GraphView> {
height: 15, height: 15,
), ),
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * width: MediaQuery.of(context).size.width * 0.9,
0.9,
height: height:
MediaQuery.of(context).size.height * MediaQuery.of(context).size.height * 0.35,
0.35,
child: (chartType == null) child: (chartType == null)
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: (chartType == 1) : (chartType == 1)
? ExpensesBarChart( ? ExpensesBarChart(
currency: currency: WalletManager
selectedWallet!.currency, .selectedWallet.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
expenseData: expenseData: generateChartData(
generateChartData(
EntryType.expense, EntryType.expense,
), ),
incomeData: const [], incomeData: const [],
) )
: Padding( : Padding(
padding: padding: const EdgeInsets.all(8),
const EdgeInsets.all(8),
child: ExpensesLineChart( child: ExpensesLineChart(
currency: selectedWallet! currency: WalletManager
.currency, .selectedWallet.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
expenseData: expenseData: generateChartData(
generateChartData(
EntryType.expense, EntryType.expense,
), ),
incomeData: const [], incomeData: const [],
@ -413,8 +397,8 @@ class _GraphViewState extends State<GraphView> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: (MediaQuery.of(context) boxShadow:
.platformBrightness == (MediaQuery.of(context).platformBrightness ==
Brightness.light) Brightness.light)
? [ ? [
BoxShadow( BoxShadow(
@ -428,12 +412,9 @@ class _GraphViewState extends State<GraphView> {
), ),
] ]
: null, : null,
color: (MediaQuery.of(context) color: (MediaQuery.of(context).platformBrightness ==
.platformBrightness ==
Brightness.dark) Brightness.dark)
? Theme.of(context) ? Theme.of(context).colorScheme.secondaryContainer
.colorScheme
.secondaryContainer
: Theme.of(context).colorScheme.surface, : Theme.of(context).colorScheme.surface,
), ),
width: MediaQuery.of(context).size.width * 0.95, width: MediaQuery.of(context).size.width * 0.95,
@ -467,23 +448,23 @@ class _GraphViewState extends State<GraphView> {
child: CategoriesPieChart( child: CategoriesPieChart(
// TODO: better size adaptivity without overflow // TODO: better size adaptivity without overflow
locale: locale ?? "en", locale: locale ?? "en",
symbol: selectedWallet!.currency.symbol, symbol: WalletManager
entries: selectedWallet!.entries .selectedWallet.currency.symbol,
entries: WalletManager.selectedWallet.entries
.where( .where(
(element) => (element) =>
((!yearly) ((!yearly)
? element.date.month == ? element.date.month ==
_selectedDate _selectedDate.month &&
.month &&
element.date.year == element.date.year ==
_selectedDate.year _selectedDate.year
: element.date.year == : element.date.year ==
_selectedDate.year) && _selectedDate.year) &&
element.type == element.type == EntryType.expense,
EntryType.expense,
) )
.toList(), .toList(),
categories: selectedWallet!.categories, categories:
WalletManager.selectedWallet.categories,
), ),
), ),
], ],
@ -496,11 +477,7 @@ class _GraphViewState extends State<GraphView> {
), // Expense Tab END ), // Expense Tab END
SingleChildScrollView( SingleChildScrollView(
child: Center( child: Center(
child: (selectedWallet == null) child: SizedBox(
? const CircularProgressIndicator(
strokeWidth: 5,
)
: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: Column( child: Column(
@ -509,8 +486,7 @@ class _GraphViewState extends State<GraphView> {
SizedBox( SizedBox(
width: 200, width: 200,
child: Row( child: Row(
mainAxisAlignment: mainAxisAlignment: MainAxisAlignment.spaceBetween,
MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
AppLocalizations.of(context).monthly, AppLocalizations.of(context).monthly,
@ -522,8 +498,7 @@ class _GraphViewState extends State<GraphView> {
value: yearly, value: yearly,
onChanged: (v) async { onChanged: (v) async {
yearly = v; yearly = v;
final s = final s = await SharedPreferences.getInstance();
await SharedPreferences.getInstance();
chartType = yearly chartType = yearly
? (s.getInt("yearlygraph") ?? 1) ? (s.getInt("yearlygraph") ?? 1)
: (s.getInt("monthlygraph") ?? 2); : (s.getInt("monthlygraph") ?? 2);
@ -543,8 +518,8 @@ class _GraphViewState extends State<GraphView> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: (MediaQuery.of(context) boxShadow:
.platformBrightness == (MediaQuery.of(context).platformBrightness ==
Brightness.light) Brightness.light)
? [ ? [
BoxShadow( BoxShadow(
@ -558,12 +533,9 @@ class _GraphViewState extends State<GraphView> {
), ),
] ]
: null, : null,
color: (MediaQuery.of(context) color: (MediaQuery.of(context).platformBrightness ==
.platformBrightness ==
Brightness.dark) Brightness.dark)
? Theme.of(context) ? Theme.of(context).colorScheme.secondaryContainer
.colorScheme
.secondaryContainer
: Theme.of(context).colorScheme.surface, : Theme.of(context).colorScheme.surface,
), ),
child: Padding( child: Padding(
@ -590,17 +562,15 @@ class _GraphViewState extends State<GraphView> {
height: 15, height: 15,
), ),
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * width: MediaQuery.of(context).size.width * 0.9,
0.9,
height: height:
MediaQuery.of(context).size.height * MediaQuery.of(context).size.height * 0.35,
0.35,
child: (chartType == null) child: (chartType == null)
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: (chartType == 1) : (chartType == 1)
? ExpensesBarChart( ? ExpensesBarChart(
currency: currency: WalletManager
selectedWallet!.currency, .selectedWallet.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
@ -610,17 +580,15 @@ class _GraphViewState extends State<GraphView> {
), ),
) )
: Padding( : Padding(
padding: padding: const EdgeInsets.all(8),
const EdgeInsets.all(8),
child: ExpensesLineChart( child: ExpensesLineChart(
currency: selectedWallet! currency: WalletManager
.currency, .selectedWallet.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
expenseData: const [], expenseData: const [],
incomeData: incomeData: generateChartData(
generateChartData(
EntryType.income, EntryType.income,
), ),
), ),
@ -636,8 +604,8 @@ class _GraphViewState extends State<GraphView> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
boxShadow: (MediaQuery.of(context) boxShadow:
.platformBrightness == (MediaQuery.of(context).platformBrightness ==
Brightness.light) Brightness.light)
? [ ? [
BoxShadow( BoxShadow(
@ -651,12 +619,9 @@ class _GraphViewState extends State<GraphView> {
), ),
] ]
: null, : null,
color: (MediaQuery.of(context) color: (MediaQuery.of(context).platformBrightness ==
.platformBrightness ==
Brightness.dark) Brightness.dark)
? Theme.of(context) ? Theme.of(context).colorScheme.secondaryContainer
.colorScheme
.secondaryContainer
: Theme.of(context).colorScheme.surface, : Theme.of(context).colorScheme.surface,
), ),
width: MediaQuery.of(context).size.width * 0.95, width: MediaQuery.of(context).size.width * 0.95,
@ -688,23 +653,23 @@ class _GraphViewState extends State<GraphView> {
padding: const EdgeInsets.all(6), padding: const EdgeInsets.all(6),
child: CategoriesPieChart( child: CategoriesPieChart(
locale: locale ?? "en", locale: locale ?? "en",
symbol: selectedWallet!.currency.symbol, symbol: WalletManager
entries: selectedWallet!.entries .selectedWallet.currency.symbol,
entries: WalletManager.selectedWallet.entries
.where( .where(
(element) => (element) =>
((!yearly) ((!yearly)
? element.date.month == ? element.date.month ==
_selectedDate _selectedDate.month &&
.month &&
element.date.year == element.date.year ==
_selectedDate.year _selectedDate.year
: element.date.year == : element.date.year ==
_selectedDate.year) && _selectedDate.year) &&
element.type == element.type == EntryType.income,
EntryType.income,
) )
.toList(), .toList(),
categories: selectedWallet!.categories, categories:
WalletManager.selectedWallet.categories,
), ),
), ),
], ],

View file

@ -94,8 +94,10 @@ class _HomeViewState extends State<HomeView> {
setState(() {}); setState(() {});
}, },
child: Scaffold( child: Scaffold(
drawer: makeDrawer(context, 1), drawer: makeDrawer(context, Pages.home),
floatingActionButton: SpeedDial( floatingActionButton: SpeedDial(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.all(Radius.circular(16)),
), ),

View file

@ -31,6 +31,7 @@ class _InitializationScreenState extends State<InitializationScreen> {
.pushReplacement(platformRoute((c) => const SetupView())); .pushReplacement(platformRoute((c) => const SetupView()));
return; return;
} }
WalletManager.selectedWallet = wallets.first;
Navigator.of(context) Navigator.of(context)
.pushReplacement(platformRoute((c) => const HomeView())); .pushReplacement(platformRoute((c) => const HomeView()));
}); });

View file

@ -64,7 +64,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
drawer: makeDrawer(context, 3), drawer: makeDrawer(context, Pages.recurringEntries),
appBar: AppBar( appBar: AppBar(
title: DropdownButton<int>( title: DropdownButton<int>(
value: (selectedWallet == null) value: (selectedWallet == null)

View file

@ -167,6 +167,9 @@ class _SetupViewState extends State<SetupView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
return; return;
} }
WalletManager.selectedWallet = wallet;
if (!context.mounted) return; if (!context.mounted) return;
unawaited( unawaited(
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(