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

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,90 +273,98 @@ 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)
leading: GestureDetector( ? const SizedBox()
onTap: () async { : ListTile(
final icon = leading: GestureDetector(
await FlutterIconPicker.showIconPicker( onTap: () async {
context, final icon =
); await FlutterIconPicker.showIconPicker(
if (icon == null) return; context,
categories[i].icon = icon; );
setState(() {}); if (icon == null) return;
}, categories[i].icon = icon;
child: Container( setState(() {});
decoration: BoxDecoration( },
borderRadius: BorderRadius.circular(16), child: Container(
color: Theme.of(context).colorScheme.secondary, decoration: BoxDecoration(
), borderRadius: BorderRadius.circular(16),
child: Padding( color: Theme.of(context)
padding: const EdgeInsets.all(8), .colorScheme
child: Icon( .secondary,
categories[i].icon, ),
color: child: Padding(
Theme.of(context).colorScheme.onSecondary, padding: const EdgeInsets.all(8),
), child: Icon(
), categories[i].icon,
), color: Theme.of(context)
), .colorScheme
trailing: IconButton( .onSecondary,
icon: const Icon(Icons.cancel),
onPressed: () {
categories.removeAt(i);
setState(() {});
},
),
title: GestureDetector(
onTap: () {
final controller = TextEditingController(
text: categories[i].name,
);
showDialog(
context: context,
builder: (c) => PlatformDialog(
actions: [
TextButton(
onPressed: () {
if (controller.text.isEmpty) return;
categories[i].name = controller.text;
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).ok,
), ),
), ),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).cancel,
),
),
],
title: AppLocalizations.of(context)
.setupCategoriesEditingName,
content: SizedBox(
width: 400,
child:
PlatformField(controller: controller),
), ),
), ),
); trailing: IconButton(
}, icon: const Icon(Icons.cancel),
child: Text( onPressed: () {
categories[i].name, categories.removeAt(i);
style: setState(() {});
const TextStyle(fontWeight: FontWeight.bold), },
), ),
), title: GestureDetector(
), onTap: () {
final controller = TextEditingController(
text: categories[i].name,
);
showDialog(
context: context,
builder: (c) => PlatformDialog(
actions: [
TextButton(
onPressed: () {
if (controller.text.isEmpty)
return;
categories[i].name =
controller.text;
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).ok,
),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context)
.cancel,
),
),
],
title: AppLocalizations.of(context)
.setupCategoriesEditingName,
content: SizedBox(
width: 400,
child: PlatformField(
controller: controller),
),
),
);
},
child: Text(
categories[i].name,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
),
),
itemCount: categories.length, itemCount: categories.length,
), ),
), ),
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,