fix: make edit_categories actually edit categories

Also make sure home loads the wallet again after exiting settings. Also removed 'type' from Category, because I don't know what it was supposed to do there.
This commit is contained in:
Matyáš Caras 2023-12-31 12:41:10 +01:00
parent 480c4e2538
commit 238caf9203
Signed by untrusted user who does not match committer: hernik
GPG key ID: 2A3175F98820C5C6
13 changed files with 275 additions and 216 deletions

View file

@ -1,6 +1,7 @@
# 1.0.0-alpha+3 # 1.0.0-alpha+3
- Add settings view for editing wallet categories - Add settings view for editing wallet categories
- Change code according to more aggressive linting - 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 # 1.0.0-alpha+2
- Fixed localization issues - Fixed localization issues
- Added graphs for expenses and income per month/year - Added graphs for expenses and income per month/year

View file

@ -9,19 +9,14 @@ class WalletCategory {
/// Represents a category in a user's wallet /// Represents a category in a user's wallet
WalletCategory({ WalletCategory({
required this.name, required this.name,
required this.type,
required this.id, required this.id,
required this.icon, required this.icon,
}); });
/// Connect the generated [_$WalletEntry] function to the `fromJson` /// Connects generated fromJson method
/// factory.
factory WalletCategory.fromJson(Map<String, dynamic> json) => factory WalletCategory.fromJson(Map<String, dynamic> json) =>
_$WalletCategoryFromJson(json); _$WalletCategoryFromJson(json);
/// Expense or income
final EntryType type;
/// User-defined name /// User-defined name
String name; String name;
@ -32,7 +27,7 @@ class WalletCategory {
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson) @JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
IconData icon; IconData icon;
/// Connect the generated [_$PersonToJson] function to the `toJson` method. /// Connects generated toJson method
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this); Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
@override @override

View file

@ -9,20 +9,13 @@ part of 'category.dart';
WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) => WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) =>
WalletCategory( WalletCategory(
name: json['name'] as String, name: json['name'] as String,
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
id: json['id'] as int, id: json['id'] as int,
icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>), icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) => Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) =>
<String, dynamic>{ <String, dynamic>{
'type': _$EntryTypeEnumMap[instance.type]!,
'name': instance.name, 'name': instance.name,
'id': instance.id, 'id': instance.id,
'icon': _iconDataToJson(instance.icon), 'icon': _iconDataToJson(instance.icon),
}; };
const _$EntryTypeEnumMap = {
EntryType.expense: 'expense',
EntryType.income: 'income',
};

View file

@ -7,7 +7,7 @@ class EntryData {
/// Contains raw data /// Contains raw data
EntryData({required this.name, required this.amount, this.description = ""}); EntryData({required this.name, required this.amount, this.description = ""});
/// Connects generated fromJson function /// Connects generated fromJson method
factory EntryData.fromJson(Map<String, dynamic> json) => factory EntryData.fromJson(Map<String, dynamic> json) =>
_$EntryDataFromJson(json); _$EntryDataFromJson(json);
@ -20,6 +20,6 @@ class EntryData {
/// Amount for entry /// Amount for entry
double amount; double amount;
/// Connects generated toJson function /// Connects generated toJson method
Map<String, dynamic> toJson() => _$EntryDataToJson(this); Map<String, dynamic> toJson() => _$EntryDataToJson(this);
} }

View file

@ -2,6 +2,7 @@ import 'package:currency_picker/currency_picker.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/walletentry.dart'; import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/walletmanager.dart';
part 'wallet.g.dart'; part 'wallet.g.dart';
Currency _currencyFromJson(Map<String, dynamic> data) => Currency _currencyFromJson(Map<String, dynamic> data) =>
@ -13,14 +14,15 @@ Currency _currencyFromJson(Map<String, dynamic> data) =>
@JsonSerializable() @JsonSerializable()
class Wallet { class Wallet {
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s /// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
Wallet( Wallet({
{required this.name, required this.name,
required this.currency, required this.currency,
this.categories = const [], this.categories = const [],
this.entries = const [], this.entries = const [],
this.starterBalance = 0,}); this.starterBalance = 0,
});
/// Connects generated fromJson function /// Connects generated fromJson method
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json); factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
/// Name of the wallet /// Name of the wallet
@ -41,10 +43,10 @@ class Wallet {
@JsonKey(fromJson: _currencyFromJson) @JsonKey(fromJson: _currencyFromJson)
final Currency currency; final Currency currency;
/// Connects generated toJson function /// Connects generated toJson method
Map<String, dynamic> toJson() => _$WalletToJson(this); Map<String, dynamic> 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 { int get nextId {
var id = 1; var id = 1;
while (entries.where((element) => element.id == id).isNotEmpty) { while (entries.where((element) => element.id == id).isNotEmpty) {
@ -53,6 +55,33 @@ class Wallet {
return id; 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<void> 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 /// Empty wallet used for placeholders
static final Wallet empty = Wallet( static final Wallet empty = Wallet(
name: "Empty", name: "Empty",

View file

@ -9,14 +9,15 @@ part 'walletentry.g.dart';
/// This is an entry containing a single item /// This is an entry containing a single item
class WalletSingleEntry { class WalletSingleEntry {
/// This is an entry containing a single item /// This is an entry containing a single item
WalletSingleEntry( WalletSingleEntry({
{required this.data, required this.data,
required this.type, required this.type,
required this.date, required this.date,
required this.category, required this.category,
required this.id,}); required this.id,
});
/// Connects generated fromJson function /// Connects generated fromJson method
factory WalletSingleEntry.fromJson(Map<String, dynamic> json) => factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
_$WalletSingleEntryFromJson(json); _$WalletSingleEntryFromJson(json);
@ -35,6 +36,6 @@ class WalletSingleEntry {
/// Unique entry ID /// Unique entry ID
int id; int id;
/// Connects generated toJson function /// Connects generated toJson method
Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this); Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this);
} }

View file

@ -54,7 +54,6 @@ class WalletManager {
} }
// if (!wallet.existsSync()) return false; // if (!wallet.existsSync()) return false;
wallet.writeAsStringSync(jsonEncode(w.toJson())); wallet.writeAsStringSync(jsonEncode(w.toJson()));
logger.i(wallet.existsSync());
return true; return true;
} }

View file

@ -76,6 +76,8 @@
"enableYou":"Povolit Material You (Může vyžadovat restart aplikace)", "enableYou":"Povolit Material You (Může vyžadovat restart aplikace)",
"enableYouDesc":"Aplikace použije barevné schéma z vaší tapety", "enableYouDesc":"Aplikace použije barevné schéma z vaší tapety",
"editCategories":"Upravit kategorie", "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"
} }

View file

@ -156,5 +156,7 @@
"enableYou":"Enable Material You (May require an app restart)", "enableYou":"Enable Material You (May require an app restart)",
"enableYouDesc":"The app will use a color scheme from your wallpaper", "enableYouDesc":"The app will use a color scheme from your wallpaper",
"editCategories":"Edit categories", "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"
} }

View file

@ -198,11 +198,16 @@ class _HomeViewState extends State<HomeView> {
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
onSelected: (value) { onSelected: (value) {
if (value == AppLocalizations.of(context).settings) { if (value == AppLocalizations.of(context).settings) {
Navigator.of(context).push( Navigator.of(context)
.push(
platformRoute( platformRoute(
(context) => const SettingsView(), (context) => const SettingsView(),
), ),
); )
.then((value) async {
selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name);
});
} else if (value == AppLocalizations.of(context).about) { } else if (value == AppLocalizations.of(context).about) {
showAboutDialog( showAboutDialog(
context: context, context: context,

View file

@ -27,7 +27,6 @@ class EditCategoriesView extends StatefulWidget {
class _EditCategoriesViewState extends State<EditCategoriesView> { class _EditCategoriesViewState extends State<EditCategoriesView> {
Wallet? selectedWallet; Wallet? selectedWallet;
List<Wallet> wallets = []; List<Wallet> wallets = [];
List<WalletCategory> categories = [];
@override @override
void initState() { void initState() {
@ -45,7 +44,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
return; return;
} }
selectedWallet = wallets.first; selectedWallet = wallets.first;
categories = selectedWallet!.categories; logger.i(selectedWallet!.categories);
setState(() {}); setState(() {});
} }
@ -115,7 +114,9 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
), ),
body: Column( body: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: selectedWallet == null
? [const CircularProgressIndicator()]
: [
Text( Text(
AppLocalizations.of(context).setupCategoriesEditHint, AppLocalizations.of(context).setupCategoriesEditHint,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -124,95 +125,114 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
height: MediaQuery.of(context).size.height * 0.64, height: MediaQuery.of(context).size.height * 0.64,
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, i) => ListTile( itemBuilder: (context, i) => (i == 0)
? const SizedBox()
: ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () async { onTap: () async {
final icon = final icon =
await FlutterIconPicker.showIconPicker(context); await FlutterIconPicker.showIconPicker(
context,
);
if (icon == null) return; if (icon == null) return;
categories[i].icon = icon; selectedWallet!.categories[i].icon = icon;
await WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.secondary, color:
Theme.of(context).colorScheme.secondary,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Icon( child: Icon(
categories[i].icon, selectedWallet!.categories[i].icon,
color: Theme.of(context).colorScheme.onSecondary, color: Theme.of(context)
.colorScheme
.onSecondary,
), ),
), ),
), ),
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.cancel), icon: const Icon(Icons.cancel),
onPressed: () { onPressed: () async {
categories.removeAt(i); await selectedWallet!.removeCategory(
selectedWallet!.categories[i],
);
setState(() {}); setState(() {});
}, },
), ),
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
final controller = final controller = TextEditingController(
TextEditingController(text: categories[i].name); text: selectedWallet!.categories[i].name,
);
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () async {
if (controller.text.isEmpty) return; if (controller.text.isEmpty) return;
categories[i].name = controller.text; selectedWallet!.categories[i].name =
controller.text;
await WalletManager.saveWallet(
selectedWallet!,
);
if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(AppLocalizations.of(context).ok), child: Text(
AppLocalizations.of(context).ok,
),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(AppLocalizations.of(context).cancel), child: Text(
AppLocalizations.of(context).cancel,
),
), ),
], ],
title: AppLocalizations.of(context) title: AppLocalizations.of(context)
.setupCategoriesEditingName, .setupCategoriesEditingName,
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: PlatformField(controller: controller), child:
PlatformField(controller: controller),
), ),
), ),
); );
}, },
child: Text( child: Text(
categories[i].name, selectedWallet!.categories[i].name,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(
fontWeight: FontWeight.bold,
), ),
), ),
), ),
itemCount: categories.length, ),
itemCount: selectedWallet!.categories.length,
), ),
), ),
IconButton( IconButton(
onPressed: () { onPressed: () async {
var id = 1; selectedWallet!.categories.add(
while (
categories.where((element) => element.id == id).isNotEmpty) {
id++; // create unique ID
}
categories.add(
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).setupWalletNamePlaceholder, name: AppLocalizations.of(context)
type: EntryType.expense, .setupWalletNamePlaceholder,
id: id, id: selectedWallet!.nextCategoryId,
icon: IconData( icon: IconData(
Icons.question_mark.codePoint, Icons.question_mark.codePoint,
fontFamily: 'MaterialIcons', fontFamily: 'MaterialIcons',
), ),
), ),
); );
await WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View file

@ -43,6 +43,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
sections: [ sections: [
SettingsSection( SettingsSection(
title: Text(AppLocalizations.of(context).wallet),
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
title: Text(AppLocalizations.of(context).editCategories), title: Text(AppLocalizations.of(context).editCategories),

View file

@ -51,9 +51,16 @@ class _SetupViewState extends State<SetupView> {
super.didChangeDependencies(); super.didChangeDependencies();
if (categories.isEmpty) { if (categories.isEmpty) {
categories = [ categories = [
WalletCategory(
name: AppLocalizations.of(context).noCategory,
id: 0,
icon: IconData(
Icons.payments.codePoint,
fontFamily: 'MaterialIcons',
),
),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryHealth, name: AppLocalizations.of(context).categoryHealth,
type: EntryType.expense,
id: 1, id: 1,
icon: IconData( icon: IconData(
Icons.medical_information.codePoint, Icons.medical_information.codePoint,
@ -62,21 +69,18 @@ class _SetupViewState extends State<SetupView> {
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryCar, name: AppLocalizations.of(context).categoryCar,
type: EntryType.expense,
id: 2, id: 2,
icon: icon:
IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryFood, name: AppLocalizations.of(context).categoryFood,
type: EntryType.expense,
id: 3, id: 3,
icon: icon:
IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryTravel, name: AppLocalizations.of(context).categoryTravel,
type: EntryType.expense,
id: 4, id: 4,
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
), ),
@ -269,7 +273,9 @@ class _SetupViewState extends State<SetupView> {
height: MediaQuery.of(context).size.height * 0.64, height: MediaQuery.of(context).size.height * 0.64,
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, i) => ListTile( itemBuilder: (context, i) => (i == 0)
? const SizedBox()
: ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () async { onTap: () async {
final icon = final icon =
@ -283,14 +289,17 @@ class _SetupViewState extends State<SetupView> {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context)
.colorScheme
.secondary,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Icon( child: Icon(
categories[i].icon, categories[i].icon,
color: color: Theme.of(context)
Theme.of(context).colorScheme.onSecondary, .colorScheme
.onSecondary,
), ),
), ),
), ),
@ -313,8 +322,10 @@ class _SetupViewState extends State<SetupView> {
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
if (controller.text.isEmpty) return; if (controller.text.isEmpty)
categories[i].name = controller.text; return;
categories[i].name =
controller.text;
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
@ -326,7 +337,8 @@ class _SetupViewState extends State<SetupView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
AppLocalizations.of(context).cancel, AppLocalizations.of(context)
.cancel,
), ),
), ),
], ],
@ -334,16 +346,16 @@ class _SetupViewState extends State<SetupView> {
.setupCategoriesEditingName, .setupCategoriesEditingName,
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: child: PlatformField(
PlatformField(controller: controller), controller: controller),
), ),
), ),
); );
}, },
child: Text( child: Text(
categories[i].name, categories[i].name,
style: style: const TextStyle(
const TextStyle(fontWeight: FontWeight.bold), fontWeight: FontWeight.bold),
), ),
), ),
), ),
@ -352,7 +364,7 @@ class _SetupViewState extends State<SetupView> {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
var id = 1; var id = 0;
while (categories while (categories
.where((element) => element.id == id) .where((element) => element.id == id)
.isNotEmpty) { .isNotEmpty) {
@ -362,7 +374,6 @@ class _SetupViewState extends State<SetupView> {
WalletCategory( WalletCategory(
name: AppLocalizations.of(context) name: AppLocalizations.of(context)
.setupWalletNamePlaceholder, .setupWalletNamePlaceholder,
type: EntryType.expense,
id: id, id: id,
icon: IconData( icon: IconData(
Icons.question_mark.codePoint, Icons.question_mark.codePoint,