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
- Add settings view for editing wallet categories
- Change code according to more aggressive linting
- Create a default "no category" category, mainly to store entries with removed categories
# 1.0.0-alpha+2
- Fixed localization issues
- Added graphs for expenses and income per month/year

View file

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

View file

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

View file

@ -7,7 +7,7 @@ class EntryData {
/// Contains raw data
EntryData({required this.name, required this.amount, this.description = ""});
/// Connects generated fromJson function
/// Connects generated fromJson method
factory EntryData.fromJson(Map<String, dynamic> json) =>
_$EntryDataFromJson(json);
@ -20,6 +20,6 @@ class EntryData {
/// Amount for entry
double amount;
/// Connects generated toJson function
/// Connects generated toJson method
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:prasule/api/category.dart';
import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/walletmanager.dart';
part 'wallet.g.dart';
Currency _currencyFromJson(Map<String, dynamic> data) =>
@ -13,14 +14,15 @@ Currency _currencyFromJson(Map<String, dynamic> data) =>
@JsonSerializable()
class Wallet {
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
Wallet(
{required this.name,
required this.currency,
this.categories = const [],
this.entries = const [],
this.starterBalance = 0,});
Wallet({
required this.name,
required this.currency,
this.categories = const [],
this.entries = const [],
this.starterBalance = 0,
});
/// Connects generated fromJson function
/// Connects generated fromJson method
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
/// Name of the wallet
@ -41,10 +43,10 @@ class Wallet {
@JsonKey(fromJson: _currencyFromJson)
final Currency currency;
/// Connects generated toJson function
/// Connects generated toJson method
Map<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 {
var id = 1;
while (entries.where((element) => element.id == id).isNotEmpty) {
@ -53,6 +55,33 @@ class Wallet {
return id;
}
/// Getter for the next unused unique number ID in the wallet's **category**
/// list
int get nextCategoryId {
var id = 0;
while (categories.where((element) => element.id == id).isNotEmpty) {
id++; // create unique ID
}
return id;
}
/// Removes the specified category.
///
/// All [WalletSingleEntry]s will have their category reassigned
/// to the default *No category*
Future<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
static final Wallet empty = Wallet(
name: "Empty",

View file

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

View file

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

View file

@ -76,6 +76,8 @@
"enableYou":"Povolit Material You (Může vyžadovat restart aplikace)",
"enableYouDesc":"Aplikace použije barevné schéma z vaší tapety",
"editCategories":"Upravit kategorie",
"editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky"
"editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky",
"wallet":"Peněženka",
"noCategory":"Žádná kategorie"
}

View file

@ -156,5 +156,7 @@
"enableYou":"Enable Material You (May require an app restart)",
"enableYouDesc":"The app will use a color scheme from your wallpaper",
"editCategories":"Edit categories",
"editCategoriesDesc":"Add, edit or remove categories from a wallet"
"editCategoriesDesc":"Add, edit or remove categories from a wallet",
"wallet":"Wallet",
"noCategory":"No category"
}

View file

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

View file

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

View file

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

View file

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