diff --git a/lib/api/category.dart b/lib/api/category.dart index e3437b3..29e641f 100644 --- a/lib/api/category.dart +++ b/lib/api/category.dart @@ -25,6 +25,11 @@ class WalletCategory { /// Connect the generated [_$PersonToJson] function to the `toJson` method. Map toJson() => _$WalletCategoryToJson(this); + + @override + String toString() { + return name; + } } Map _iconDataToJson(IconData icon) => diff --git a/lib/api/entry.dart b/lib/api/entry.dart index 9258f0a..cf0e027 100644 --- a/lib/api/entry.dart +++ b/lib/api/entry.dart @@ -9,19 +9,21 @@ class WalletEntry { double amount; DateTime date; WalletCategory category; + int id; WalletEntry( {required this.name, required this.amount, required this.type, required this.date, - required this.category}); + required this.category, + required this.id}); /// Connect the generated [_$WalletEntry] function to the `fromJson` /// factory. factory WalletEntry.fromJson(Map json) => _$WalletEntryFromJson(json); - /// Connect the generated [_$PersonToJson] function to the `toJson` method. + /// Connect the generated [_$WalletEntryToJson] function to the `toJson` method. Map toJson() => _$WalletEntryToJson(this); } diff --git a/lib/api/entry.g.dart b/lib/api/entry.g.dart index 91fcc73..80ada6e 100644 --- a/lib/api/entry.g.dart +++ b/lib/api/entry.g.dart @@ -13,6 +13,7 @@ WalletEntry _$WalletEntryFromJson(Map json) => WalletEntry( date: DateTime.parse(json['date'] as String), category: WalletCategory.fromJson(json['category'] as Map), + id: json['id'] as int, ); Map _$WalletEntryToJson(WalletEntry instance) => @@ -22,6 +23,7 @@ Map _$WalletEntryToJson(WalletEntry instance) => 'amount': instance.amount, 'date': instance.date.toIso8601String(), 'category': instance.category, + 'id': instance.id, }; const _$EntryTypeEnumMap = { diff --git a/lib/pw/platformfield.dart b/lib/pw/platformfield.dart index 98ecb68..b038858 100644 --- a/lib/pw/platformfield.dart +++ b/lib/pw/platformfield.dart @@ -27,7 +27,7 @@ class PlatformField extends PlatformWidget { this.onChanged, this.autofillHints, this.textStyle, - this.textAlign = TextAlign.center}); + this.textAlign = TextAlign.start}); @override TextField createAndroidWidget(BuildContext context) => TextField( diff --git a/lib/views/create_entry.dart b/lib/views/create_entry.dart index 68bc79d..4762fa6 100644 --- a/lib/views/create_entry.dart +++ b/lib/views/create_entry.dart @@ -9,7 +9,8 @@ import 'package:prasule/pw/platformfield.dart'; class CreateEntryView extends StatefulWidget { final Wallet w; - const CreateEntryView({super.key, required this.w}); + final WalletEntry? editEntry; + const CreateEntryView({super.key, required this.w, this.editEntry}); @override State createState() => _CreateEntryViewState(); @@ -20,12 +21,21 @@ class _CreateEntryViewState extends State { @override void initState() { super.initState(); - newEntry = WalletEntry( - name: "", - amount: 0, - type: EntryType.expense, - date: DateTime.now(), - category: widget.w.categories.first); + if (widget.editEntry != null) { + newEntry = widget.editEntry!; + } else { + var id = 1; + while (widget.w.entries.where((element) => element.id == id).isNotEmpty) { + id++; // create unique ID + } + newEntry = WalletEntry( + id: id, + name: "", + amount: 0, + type: EntryType.expense, + date: DateTime.now(), + category: widget.w.categories.first); + } setState(() {}); } @@ -36,7 +46,7 @@ class _CreateEntryViewState extends State { title: const Text("Create new entry"), ), body: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, + width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, child: Center( child: SingleChildScrollView( @@ -45,8 +55,9 @@ class _CreateEntryViewState extends State { children: [ const Text("Name"), SizedBox( - width: MediaQuery.of(context).size.width * 0.4, + width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( + controller: TextEditingController(text: newEntry.name), onChanged: (v) { newEntry.name = v; }, @@ -57,13 +68,15 @@ class _CreateEntryViewState extends State { ), const Text("Amount"), SizedBox( - width: MediaQuery.of(context).size.width * 0.4, + width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( - controller: TextEditingController(text: "0"), + controller: + TextEditingController(text: newEntry.amount.toString()), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'\d+[\.,]\d+')) + FilteringTextInputFormatter.allow( + RegExp(r'\d+[\.,]{0,1}\d{0,}')) ], onChanged: (v) { newEntry.amount = double.parse(v); @@ -78,24 +91,23 @@ class _CreateEntryViewState extends State { height: 10, ), SizedBox( - width: MediaQuery.of(context).size.width * 0.4, + width: MediaQuery.of(context).size.width * 0.8, child: DropdownButton( value: newEntry.type, items: [ DropdownMenuItem( value: EntryType.expense, child: SizedBox( - width: MediaQuery.of(context).size.width * 0.4 - 24, + width: MediaQuery.of(context).size.width * 0.8 - 24, child: const Text( "Expense", - textAlign: TextAlign.center, ), ), ), DropdownMenuItem( value: EntryType.income, child: SizedBox( - width: MediaQuery.of(context).size.width * 0.4 - 24, + width: MediaQuery.of(context).size.width * 0.8 - 24, child: const Text("Income"), ), ), @@ -115,26 +127,26 @@ class _CreateEntryViewState extends State { height: 10, ), SizedBox( - width: MediaQuery.of(context).size.width * 0.4, + width: MediaQuery.of(context).size.width * 0.8, child: DropdownButton( - value: widget.w.categories.indexOf(newEntry.category), + value: newEntry.category.id, items: List.generate( widget.w.categories.length, (index) => DropdownMenuItem( - value: widget.w.categories - .indexOf(widget.w.categories[index]), + value: widget.w.categories[index].id, child: SizedBox( - width: MediaQuery.of(context).size.width * 0.4 - 24, + width: MediaQuery.of(context).size.width * 0.8 - 24, child: Text( widget.w.categories[index].name, - textAlign: TextAlign.center, ), ), ), ), onChanged: (v) { if (v == null) return; - newEntry.category = widget.w.categories[v]; + newEntry.category = widget.w.categories + .where((element) => element.id == v) + .first; setState(() {}); }, ), @@ -154,6 +166,10 @@ class _CreateEntryViewState extends State { ); return; } + if (widget.editEntry != null) { + Navigator.of(context).pop(newEntry); + return; + } widget.w.entries.add(newEntry); WalletManager.saveWallet(widget.w).then((value) => Navigator.of(context) diff --git a/lib/views/home.dart b/lib/views/home.dart index 568f805..dd1529c 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,16 +1,16 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; -import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:grouped_list/grouped_list.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; +import 'package:prasule/api/entry.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/main.dart'; import 'package:prasule/network/tessdata.dart'; +import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/views/create_entry.dart'; import 'package:prasule/views/settings/settings.dart'; @@ -32,6 +32,11 @@ class _HomeViewState extends State { super.didChangeDependencies(); locale = Localizations.localeOf(context).languageCode; initializeDateFormatting(Localizations.localeOf(context).languageCode); + } + + @override + void initState() { + super.initState(); loadWallet(); } @@ -57,11 +62,14 @@ class _HomeViewState extends State { child: const Icon(Icons.edit), label: "Add new", onTap: () async { - selectedWallet = await Navigator.of(context).push( + var sw = await Navigator.of(context).push( MaterialPageRoute( builder: (c) => CreateEntryView(w: selectedWallet!), ), ); + if (sw != null) { + selectedWallet = sw; + } setState(() {}); }), SpeedDialChild( @@ -186,22 +194,91 @@ class _HomeViewState extends State { elements: selectedWallet!.entries ..sort((a, b) => a.date.compareTo(b.date)), groupBy: (e) => DateFormat.yMMMM(locale).format(e.date), - itemBuilder: (context, element) => ListTile( - leading: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.secondary), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - element.category.icon, - color: Theme.of(context).colorScheme.onSecondary, + itemBuilder: (context, element) => Slidable( + endActionPane: + ActionPane(motion: const ScrollMotion(), children: [ + SlidableAction( + onPressed: (c) { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (c) => CreateEntryView( + w: selectedWallet!, + editEntry: element, + ), + ), + ) + .then( + (editedEntry) { + if (editedEntry == null) return; + selectedWallet!.entries.remove(element); + selectedWallet!.entries.add(editedEntry); + WalletManager.saveWallet(selectedWallet!); + setState(() {}); + }, + ); + }, + backgroundColor: + Theme.of(context).colorScheme.secondary, + foregroundColor: + Theme.of(context).colorScheme.onSecondary, + icon: Icons.edit, + ), + SlidableAction( + backgroundColor: + Theme.of(context).colorScheme.error, + foregroundColor: + Theme.of(context).colorScheme.onError, + icon: Icons.delete, + onPressed: (c) { + showDialog( + context: context, + builder: (cx) => PlatformDialog( + title: "Are you sure", + content: const Text( + "Do you really want to delete this entry?"), + actions: [ + PlatformButton( + text: "Yes", + onPressed: () { + selectedWallet?.entries.removeWhere( + (e) => e.id == element.id); + WalletManager.saveWallet( + selectedWallet!); + Navigator.of(cx).pop(); + setState(() {}); + }, + ), + PlatformButton( + text: "No", + onPressed: () { + Navigator.of(cx).pop(); + }, + ), + ], + ), + ); + }, + ), + ]), + child: ListTile( + leading: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Theme.of(context).colorScheme.secondary), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + element.category.icon, + color: + Theme.of(context).colorScheme.onSecondary, + ), ), ), + title: Text(element.name), + subtitle: Text( + "${element.amount} ${selectedWallet!.currency.symbol}"), ), - title: Text(element.name), - subtitle: Text( - "${element.amount} ${selectedWallet!.currency.symbol}"), ), ), ), diff --git a/lib/views/setup.dart b/lib/views/setup.dart index c909090..1ac0d04 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -42,19 +42,19 @@ class _SetupViewState extends State { WalletCategory( name: "Car", type: EntryType.expense, - id: 1, + id: 2, icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), ), WalletCategory( name: "Food", type: EntryType.expense, - id: 1, + id: 3, icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), ), WalletCategory( name: "Travel", type: EntryType.expense, - id: 1, + id: 4, icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), ), ]; diff --git a/pubspec.lock b/pubspec.lock index 2941a42..f14d548 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -403,6 +403,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.15" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_speed_dial: dependency: "direct main" description: @@ -1094,4 +1102,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.1.0-262.2.beta <3.3.0" - flutter: ">=3.4.0-17.0.pre" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index d0968a8..7300743 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: flutter_speed_dial: ^7.0.0 image_picker: ^1.0.1 flutter_tesseract_ocr: ^0.4.23 + flutter_slidable: ^3.0.0 dev_dependencies: flutter_test: