From 3be02d3ffe8e652908e59b3a0d0c45c1dfb6e6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Fri, 29 Dec 2023 19:30:46 +0100 Subject: [PATCH 1/4] docs: fix changelog version --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a86776d..6814bd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# 1.0.0-alpha.2 +# 1.0.0-alpha+2 - Fixed localization issues - Added graphs for expenses and income per month/year # 1.0.0-alpha -- 2.45.2 From 1ce1342644a20e5a4436669c4851dcfe647b0054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Fri, 29 Dec 2023 19:43:27 +0100 Subject: [PATCH 2/4] feat(graphs): work on category pie chart --- lib/util/graphs.dart | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index 5a21330..a601667 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -4,6 +4,8 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:prasule/api/category.dart'; +import 'package:prasule/api/walletentry.dart'; /// Monthly/Yearly expense/income [LineChart] class ExpensesLineChart extends StatelessWidget { @@ -295,3 +297,30 @@ class ExpensesBarChart extends StatelessWidget { ), ); } + +class CategoriesPieChart extends StatelessWidget { + const CategoriesPieChart( + {super.key, required this.entries, required this.categories}); + + final List entries; + final List categories; + + @override + Widget build(BuildContext context) => PieChart( + PieChartData( + sections: List.generate( + categories.length, + (index) => PieChartSectionData( + value: entries + .where( + (element) => element.category.id == categories[index].id) + .fold( + 0, + (previousValue, element) => + previousValue + element.data.amount, + ), + ), + ), + ), + ); +} -- 2.45.2 From 4b035e0724928b673ef342562a121e387498e22b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Sat, 6 Jan 2024 16:08:38 +0100 Subject: [PATCH 3/4] feat: allow setting a color for a category --- CHANGELOG.md | 1 + lib/api/category.dart | 8 +++ lib/api/category.g.dart | 2 + lib/l10n/app_cs.arb | 5 +- lib/l10n/app_en.arb | 4 +- lib/util/text_color.dart | 11 ++++ lib/views/home.dart | 5 +- lib/views/settings/edit_categories.dart | 62 +++++++++++++++++--- lib/views/setup.dart | 78 +++++++++++++++++++++---- pubspec.lock | 52 +++++++++++------ pubspec.yaml | 1 + 11 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 lib/util/text_color.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f52790..e17eba5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - 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 +- Categories now have changeable colors assigned to them # 1.0.0-alpha+2 - Fixed localization issues - Added graphs for expenses and income per month/year diff --git a/lib/api/category.dart b/lib/api/category.dart index 30424f7..7088b9a 100644 --- a/lib/api/category.dart +++ b/lib/api/category.dart @@ -11,6 +11,7 @@ class WalletCategory { required this.name, required this.id, required this.icon, + required this.color, }); /// Connects generated fromJson method @@ -27,6 +28,10 @@ class WalletCategory { @JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson) IconData icon; + /// The color that will be displayed with entry + @JsonKey(fromJson: _colorFromJson, toJson: _colorToJson) + Color color; + /// Connects generated toJson method Map toJson() => _$WalletCategoryToJson(this); @@ -42,6 +47,9 @@ Map _iconDataToJson(IconData icon) => IconData _iconDataFromJson(Map data) => IconData(data['codepoint'] as int, fontFamily: data['family'] as String?); +int _colorToJson(Color color) => color.value; +Color _colorFromJson(int input) => Color(input); + /// Type of entry, either expense or income enum EntryType { /// Expense diff --git a/lib/api/category.g.dart b/lib/api/category.g.dart index f70475f..9d013a6 100644 --- a/lib/api/category.g.dart +++ b/lib/api/category.g.dart @@ -11,6 +11,7 @@ WalletCategory _$WalletCategoryFromJson(Map json) => name: json['name'] as String, id: json['id'] as int, icon: _iconDataFromJson(json['icon'] as Map), + color: _colorFromJson(json['color'] as int), ); Map _$WalletCategoryToJson(WalletCategory instance) => @@ -18,4 +19,5 @@ Map _$WalletCategoryToJson(WalletCategory instance) => 'name': instance.name, 'id': instance.id, 'icon': _iconDataToJson(instance.icon), + 'color': _colorToJson(instance.color), }; diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index c3065c1..3c2d431 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -78,6 +78,7 @@ "editCategories":"Upravit kategorie", "editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky", "wallet":"Peněženka", - "noCategory":"Žádná kategorie" - + "noCategory":"Žádná kategorie", + "done":"Hotovo", + "pickColor":"Zvolte barvu" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index eb1bf55..6aa8c91 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -158,5 +158,7 @@ "editCategories":"Edit categories", "editCategoriesDesc":"Add, edit or remove categories from a wallet", "wallet":"Wallet", - "noCategory":"No category" + "noCategory":"No category", + "done":"Done", + "pickColor":"Pick a color" } \ No newline at end of file diff --git a/lib/util/text_color.dart b/lib/util/text_color.dart new file mode 100644 index 0000000..2c9788a --- /dev/null +++ b/lib/util/text_color.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +/// Used to add [calculateTextColor] to the [Color] class +extension TextColor on Color { + /// Returns if foreground should be white or dark on this [Color] + Color calculateTextColor() { + return ThemeData.estimateBrightnessForColor(this) == Brightness.light + ? Colors.black + : Colors.white; + } +} diff --git a/lib/views/home.dart b/lib/views/home.dart index 430ccb1..a1edb1e 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -24,6 +24,7 @@ import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/util/drawer.dart'; +import 'package:prasule/util/text_color.dart'; import 'package:prasule/views/create_entry.dart'; import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/settings/tessdata_list.dart'; @@ -358,14 +359,14 @@ class _HomeViewState extends State { leading: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: Theme.of(context).colorScheme.secondary, + color: element.category.color, ), child: Padding( padding: const EdgeInsets.all(8), child: Icon( element.category.icon, color: - Theme.of(context).colorScheme.onSecondary, + element.category.color.calculateTextColor(), ), ), ), diff --git a/lib/views/settings/edit_categories.dart b/lib/views/settings/edit_categories.dart index b97afc0..98fe706 100644 --- a/lib/views/settings/edit_categories.dart +++ b/lib/views/settings/edit_categories.dart @@ -2,6 +2,8 @@ import 'dart:async'; +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_iconpicker/flutter_iconpicker.dart'; @@ -9,11 +11,14 @@ import 'package:prasule/api/category.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/main.dart'; +import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/util/text_color.dart'; import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/setup.dart'; +import 'package:shared_preferences/shared_preferences.dart'; /// Allows adding, editing or removing [WalletCategory]s class EditCategoriesView extends StatefulWidget { @@ -134,24 +139,64 @@ class _EditCategoriesViewState extends State { await FlutterIconPicker.showIconPicker( context, ); - if (icon == null) return; - selectedWallet!.categories[i].icon = icon; + if (icon != null) { + selectedWallet!.categories[i].icon = icon; + } + final materialEnabled = + (await SharedPreferences.getInstance()) + .getBool("useMaterialYou") ?? + false; + if (!mounted) return; + await showDialog( + context: context, + builder: (c) => PlatformDialog( + actions: [ + PlatformButton( + text: AppLocalizations.of(context).done, + onPressed: () { + Navigator.of(c).pop(); + }, + ), + ], + title: + AppLocalizations.of(context).pickColor, + content: Column( + children: [ + ColorPicker( + pickersEnabled: { + ColorPickerType.wheel: true, + ColorPickerType.primary: false, + ColorPickerType.custom: false, + ColorPickerType.bw: false, + ColorPickerType.accent: + materialEnabled, + }, + color: selectedWallet! + .categories[i].color, + onColorChanged: (color) { + selectedWallet! + .categories[i].color = color; + setState(() {}); + }, + ), + ], + ), + ), + ); await WalletManager.saveWallet(selectedWallet!); setState(() {}); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: - Theme.of(context).colorScheme.secondary, + color: selectedWallet!.categories[i].color, ), child: Padding( padding: const EdgeInsets.all(8), child: Icon( selectedWallet!.categories[i].icon, - color: Theme.of(context) - .colorScheme - .onSecondary, + color: selectedWallet!.categories[i].color + .calculateTextColor(), ), ), ), @@ -230,6 +275,9 @@ class _EditCategoriesViewState extends State { Icons.question_mark.codePoint, fontFamily: 'MaterialIcons', ), + color: Colors.blueGrey.harmonizeWith( + Theme.of(context).colorScheme.primary, + ), ), ); await WalletManager.saveWallet(selectedWallet!); diff --git a/lib/views/setup.dart b/lib/views/setup.dart index 567fd0a..320d12a 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -1,6 +1,8 @@ // ignore_for_file: inference_failure_on_function_invocation import 'package:currency_picker/currency_picker.dart'; +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -13,7 +15,9 @@ import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/util/text_color.dart'; import 'package:prasule/views/home.dart'; +import 'package:shared_preferences/shared_preferences.dart'; /// View that shows on first-time setup class SetupView extends StatefulWidget { @@ -58,6 +62,7 @@ class _SetupViewState extends State { Icons.payments.codePoint, fontFamily: 'MaterialIcons', ), + color: Colors.transparent, ), WalletCategory( name: AppLocalizations.of(context).categoryHealth, @@ -66,23 +71,31 @@ class _SetupViewState extends State { Icons.medical_information.codePoint, fontFamily: 'MaterialIcons', ), + color: Colors.red.shade700 + .harmonizeWith(Theme.of(context).colorScheme.primary), ), WalletCategory( name: AppLocalizations.of(context).categoryCar, id: 2, icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), + color: Colors.purple + .harmonizeWith(Theme.of(context).colorScheme.primary), ), WalletCategory( name: AppLocalizations.of(context).categoryFood, id: 3, icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), + color: Colors.green.shade700 + .harmonizeWith(Theme.of(context).colorScheme.primary), ), WalletCategory( name: AppLocalizations.of(context).categoryTravel, id: 4, icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), + color: Colors.orange.shade700 + .harmonizeWith(Theme.of(context).colorScheme.primary), ), ]; setState(() {}); @@ -282,24 +295,61 @@ class _SetupViewState extends State { await FlutterIconPicker.showIconPicker( context, ); - if (icon == null) return; - categories[i].icon = icon; + if (icon != null) categories[i].icon = icon; + final materialEnabled = + (await SharedPreferences.getInstance()) + .getBool("useMaterialYou") ?? + false; + if (!mounted) return; + await showDialog( + context: context, + builder: (c) => PlatformDialog( + actions: [ + PlatformButton( + text: AppLocalizations.of(context) + .done, + onPressed: () { + Navigator.of(c).pop(); + }, + ), + ], + title: AppLocalizations.of(context) + .pickColor, + content: Column( + children: [ + ColorPicker( + pickersEnabled: { + ColorPickerType.wheel: true, + ColorPickerType.primary: false, + ColorPickerType.custom: false, + ColorPickerType.bw: false, + ColorPickerType.accent: + materialEnabled, + }, + color: categories[i].color, + onColorChanged: (color) { + categories[i].color = color; + setState(() {}); + }, + ), + ], + ), + ), + ); setState(() {}); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), - color: Theme.of(context) - .colorScheme - .secondary, + color: categories[i].color, ), child: Padding( padding: const EdgeInsets.all(8), child: Icon( categories[i].icon, - color: Theme.of(context) - .colorScheme - .onSecondary, + color: categories[i] + .color + .calculateTextColor(), ), ), ), @@ -322,8 +372,9 @@ class _SetupViewState extends State { actions: [ TextButton( onPressed: () { - if (controller.text.isEmpty) + if (controller.text.isEmpty) { return; + } categories[i].name = controller.text; Navigator.of(context).pop(); @@ -347,7 +398,8 @@ class _SetupViewState extends State { content: SizedBox( width: 400, child: PlatformField( - controller: controller), + controller: controller, + ), ), ), ); @@ -355,7 +407,8 @@ class _SetupViewState extends State { child: Text( categories[i].name, style: const TextStyle( - fontWeight: FontWeight.bold), + fontWeight: FontWeight.bold, + ), ), ), ), @@ -379,6 +432,9 @@ class _SetupViewState extends State { Icons.question_mark.codePoint, fontFamily: 'MaterialIcons', ), + color: Colors.blueGrey.harmonizeWith( + Theme.of(context).colorScheme.primary, + ), ), ); setState(() {}); diff --git a/pubspec.lock b/pubspec.lock index 77b846c..5e8710d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -321,6 +321,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.66.0" + flex_color_picker: + dependency: "direct main" + description: + name: flex_color_picker + sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c + url: "https://pub.dev" + source: hosted + version: "3.3.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" flutter: dependency: "direct main" description: flutter @@ -531,26 +547,26 @@ packages: dependency: "direct main" description: name: image_picker - sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77 + sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe + sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.9+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7" + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" image_picker_ios: dependency: transitive description: @@ -579,10 +595,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514 + sha256: "0e827c156e3a90edd3bbe7f6de048b39247b16e58173b08a835b7eb00aba239e" url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.9.2" image_picker_windows: dependency: transitive description: @@ -760,10 +776,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72 + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_foundation: dependency: transitive description: @@ -816,10 +832,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.8" pointycastle: dependency: transitive description: @@ -1165,18 +1181,18 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" xml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e87beb4..38afb3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: dio: ^5.3.0 dynamic_color: ^1.6.6 fl_chart: ^0.66.0 + flex_color_picker: ^3.3.0 flutter: sdk: flutter flutter_iconpicker: ^3.2.4 -- 2.45.2 From d5e94d63d772c7fbf5a5c01cd0dd8d13a9467ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 8 Jan 2024 15:23:42 +0100 Subject: [PATCH 4/4] feat(graphs): complete pie chart --- CHANGELOG.md | 1 + lib/l10n/app_cs.arb | 3 +- lib/l10n/app_en.arb | 3 +- lib/pw/platformwidget.dart | 3 +- lib/util/get_last_date.dart | 8 ++ lib/util/graphs.dart | 236 +++++++++++++++++++++++++++++++----- lib/views/graph_view.dart | 166 ++++++++++++++----------- lib/views/setup.dart | 2 +- 8 files changed, 318 insertions(+), 104 deletions(-) create mode 100644 lib/util/get_last_date.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e17eba5..4097a41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Change code according to more aggressive linting - Create a default "no category" category, mainly to store entries with removed categories - Categories now have changeable colors assigned to them +- Added pie chart for expense/income data per category # 1.0.0-alpha+2 - Fixed localization issues - Added graphs for expenses and income per month/year diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 3c2d431..0c9bc09 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -80,5 +80,6 @@ "wallet":"Peněženka", "noCategory":"Žádná kategorie", "done":"Hotovo", - "pickColor":"Zvolte barvu" + "pickColor":"Zvolte barvu", + "changeDate":"Změnit ze kterého měsíce/roku brát data" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 6aa8c91..b96bd6f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -160,5 +160,6 @@ "wallet":"Wallet", "noCategory":"No category", "done":"Done", - "pickColor":"Pick a color" + "pickColor":"Pick a color", + "changeDate":"Change what month/year to pick data from" } \ No newline at end of file diff --git a/lib/pw/platformwidget.dart b/lib/pw/platformwidget.dart index 98d9c89..b48c0b0 100644 --- a/lib/pw/platformwidget.dart +++ b/lib/pw/platformwidget.dart @@ -5,7 +5,8 @@ import 'package:flutter/material.dart'; /// Abstract class used to create widgets for the respective platform UI library abstract class PlatformWidget extends StatelessWidget { - /// Abstract class used to create widgets for the respective platform UI library + /// Abstract class used to create widgets + /// for the respective platform UI library const PlatformWidget({super.key}); @override diff --git a/lib/util/get_last_date.dart b/lib/util/get_last_date.dart new file mode 100644 index 0000000..27b52ad --- /dev/null +++ b/lib/util/get_last_date.dart @@ -0,0 +1,8 @@ +/// Extension to get last day of the month +extension LastDay on DateTime { + /// Returns the last day of the month as [int] + int get lastDay { + final d = add(const Duration(days: 31)); + return DateTime(d.year, d.month, 0).day; + } +} diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index f311802..2ef91fe 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -3,9 +3,12 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; import 'package:prasule/api/walletentry.dart'; -import 'package:intl/intl.dart'; +import 'package:prasule/main.dart'; +import 'package:prasule/util/get_last_date.dart'; +import 'package:prasule/util/text_color.dart'; /// Monthly/Yearly expense/income [LineChart] class ExpensesLineChart extends StatelessWidget { @@ -72,7 +75,9 @@ class ExpensesLineChart extends StatelessWidget { getTooltipItems: (spots) => List.generate( spots.length, (index) => LineTooltipItem( - (spots[index].barIndex == 0) + // Changes what's rendered on the tooltip + // when clicked in the chart + (spots[index].barIndex == 0) // income chart ? (yearly ? AppLocalizations.of(context).incomeForMonth( DateFormat.MMMM(locale).format( @@ -94,7 +99,7 @@ class ExpensesLineChart extends StatelessWidget { name: currency.name, ).format(spots[index].y), )) - : (yearly + : (yearly // expense chart ? AppLocalizations.of(context).expensesForMonth( DateFormat.MMMM(locale).format( DateTime( @@ -116,12 +121,25 @@ class ExpensesLineChart extends StatelessWidget { ).format(spots[index].y), )), TextStyle(color: spots[index].bar.color), + children: [ + TextSpan( + text: "\n${yearly ? DateFormat.MMMM(locale).format( + DateTime( + date.year, + index + 1, + ), + ) : DateFormat.yMMMMd(locale).format(DateTime(date.year, date.month, spots[index].spotIndex + 1))}", + ), + ], ), ), ), ), maxY: maxY, - maxX: yearly ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), + maxX: yearly + ? 12 + : date.lastDay.toDouble() - + 1, // remove 1 because we are indexing from 0 minY: 0, minX: 0, backgroundColor: Theme.of(context).colorScheme.background, @@ -133,10 +151,13 @@ class ExpensesLineChart extends StatelessWidget { isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), - color: Colors.green - .harmonizeWith(Theme.of(context).colorScheme.secondary), + color: + (MediaQuery.of(context).platformBrightness == Brightness.dark) + ? Colors.green.shade300 + : Colors.green + .harmonizeWith(Theme.of(context).colorScheme.primary), spots: List.generate( - yearly ? 12 : DateTime(date.year, date.month, 0).day, + yearly ? 12 : date.lastDay, (index) => FlSpot(index.toDouble(), incomeData[index]), ), ), @@ -147,17 +168,37 @@ class ExpensesLineChart extends StatelessWidget { isStrokeCapRound: true, dotData: const FlDotData(show: false), belowBarData: BarAreaData(), - color: Colors.red - .harmonizeWith(Theme.of(context).colorScheme.secondary), + color: + (MediaQuery.of(context).platformBrightness == Brightness.dark) + ? Colors.red.shade300 + : Colors.red + .harmonizeWith(Theme.of(context).colorScheme.primary), spots: List.generate( - yearly ? 12 : DateTime(date.year, date.month, 0).day, - (index) => FlSpot(index.toDouble() + 1, expenseData[index]), + yearly + ? 12 + : date.lastDay, // no -1 because it's the length, not index + (index) => FlSpot(index.toDouble(), expenseData[index]), ), ), ], // actual data titlesData: FlTitlesData( rightTitles: const AxisTitles(), topTitles: const AxisTitles(), + leftTitles: AxisTitles( + sideTitles: SideTitles( + reservedSize: (NumberFormat.compact() + .format(expenseDataSorted.last) + .length >= + 5 || + NumberFormat.compact() + .format(incomeDataSorted.last) + .length >= + 5) + ? 50 + : 25, + showTitles: true, + ), + ), bottomTitles: AxisTitles( sideTitles: SideTitles( reservedSize: 30, @@ -319,7 +360,7 @@ class ExpensesBarChart extends StatelessWidget { minY: 0, maxY: maxY, barGroups: List.generate( - yearly ? 12 : DateTime(date.year, date.month, 0).day, + yearly ? 12 : date.lastDay - 1, (index) => BarChartGroupData( x: index, barRods: [ @@ -327,13 +368,13 @@ class ExpensesBarChart extends StatelessWidget { BarChartRodData( toY: incomeData[index], color: Colors.green - .harmonizeWith(Theme.of(context).colorScheme.secondary), + .harmonizeWith(Theme.of(context).colorScheme.primary), ), if (expenseData.isNotEmpty) BarChartRodData( toY: expenseData[index], color: Colors.red - .harmonizeWith(Theme.of(context).colorScheme.secondary), + .harmonizeWith(Theme.of(context).colorScheme.primary), ), ], ), @@ -342,29 +383,164 @@ class ExpensesBarChart extends StatelessWidget { ); } -class CategoriesPieChart extends StatelessWidget { - const CategoriesPieChart( - {super.key, required this.entries, required this.categories}); +/// [PieChart] used to display expenses/income visualized +/// under their respective category +class CategoriesPieChart extends StatefulWidget { + /// [PieChart] used to display expenses/income visualized + /// under their respective category + const CategoriesPieChart({ + required this.entries, + required this.categories, + required this.symbol, + super.key, + }); + /// Entries to be used final List entries; + + /// Categories to be displayed final List categories; + /// Currency symbol displayed on the chart + final String symbol; + @override - Widget build(BuildContext context) => PieChart( - PieChartData( - sections: List.generate( - categories.length, - (index) => PieChartSectionData( - value: entries - .where( - (element) => element.category.id == categories[index].id) - .fold( - 0, - (previousValue, element) => - previousValue + element.data.amount, + State createState() => _CategoriesPieChartState(); +} + +class _CategoriesPieChartState extends State { + int touchedIndex = -1; + + @override + Widget build(BuildContext context) => Column( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width, + child: Wrap( + alignment: WrapAlignment.center, + spacing: 4, + children: List.generate( + widget.categories.length, + (index) => Padding( + padding: const EdgeInsets.all(8), + child: Indicator( + size: touchedIndex == index ? 18 : 16, + color: widget.categories[index].color, + text: widget.categories[index].name, + textStyle: TextStyle( + fontWeight: (touchedIndex == index) + ? FontWeight.bold + : FontWeight.normal, + ), ), + ), + ), ), ), - ), + const SizedBox( + height: 5, + ), + Expanded( + child: PieChart( + PieChartData( + centerSpaceRadius: double.infinity, + pieTouchData: PieTouchData( + touchCallback: (event, response) { + // Set touchedIndex so we can highlight + // the corresponding indicator + setState(() { + if (!event.isInterestedForInteractions || + response == null || + response.touchedSection == null) { + touchedIndex = -1; + return; + } + touchedIndex = + response.touchedSection!.touchedSectionIndex; + }); + }, + ), + sections: List.generate( + widget.categories.length, + (index) => PieChartSectionData( + title: NumberFormat.compactCurrency(symbol: widget.symbol) + .format( + widget.entries + .where( + (element) => + element.category.id == + widget.categories[index].id, + ) + .fold( + 0, + (previousValue, element) => + previousValue + element.data.amount, + ), + ), + titleStyle: TextStyle( + color: + widget.categories[index].color.calculateTextColor(), + fontWeight: FontWeight.bold, + ), + color: widget.categories[index].color, + value: widget.entries + .where( + (element) => + element.category.id == + widget.categories[index].id, + ) + .fold( + 0, + (previousValue, element) => + previousValue + element.data.amount, + ), + ), + ), + ), + ), + ), + ], + ); +} + +/// Used to indicate which part of a chart is for what +class Indicator extends StatelessWidget { + /// Used to indicate which part of a chart is for what + const Indicator({ + required this.size, + required this.color, + required this.text, + this.textStyle = const TextStyle(), + super.key, + }); + + /// Size of the indicator circle + final double size; + + /// Color of the indicator circle + final Color color; + + /// Text shown next to the indicator circle + final String text; + + final TextStyle textStyle; + + @override + Widget build(BuildContext context) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + const SizedBox( + width: 4, + ), + Text(text, style: textStyle), + ], ); } diff --git a/lib/views/graph_view.dart b/lib/views/graph_view.dart index 0b1610c..37d6cb6 100644 --- a/lib/views/graph_view.dart +++ b/lib/views/graph_view.dart @@ -40,8 +40,9 @@ class _GraphViewState extends State { } List generateChartData(EntryType type) { + final d = _selectedDate.add(const Duration(days: 31)); final data = List.filled( - yearly ? 12 : DateTime(_selectedDate.year, _selectedDate.month, 0).day, + yearly ? 12 : DateTime(d.year, d.month, 0).day, 0, ); if (selectedWallet == null) return []; @@ -92,6 +93,47 @@ class _GraphViewState extends State { @override Widget build(BuildContext context) { return Scaffold( + floatingActionButton: Tooltip( + message: AppLocalizations.of(context).changeDate, + child: PlatformButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.primary, + ), + foregroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.onPrimary, + ), + ), + text: yearly + ? DateFormat.y(locale).format(_selectedDate) + : DateFormat.yMMMM(locale).format(_selectedDate), + onPressed: () async { + final firstDate = (selectedWallet!.entries + ..sort( + (a, b) => a.date.compareTo(b.date), + )) + .first + .date; + final newDate = await showDatePicker( + context: context, + initialDate: DateTime( + _selectedDate.year, + _selectedDate.month, + ), + firstDate: firstDate, + lastDate: DateTime.now(), + initialEntryMode: yearly + ? DatePickerEntryMode.input + : DatePickerEntryMode.calendar, + initialDatePickerMode: + yearly ? DatePickerMode.year : DatePickerMode.day, + ); + if (newDate == null) return; + _selectedDate = newDate; + setState(() {}); + }, + ), + ), appBar: AppBar( title: DropdownButton( value: @@ -137,11 +179,19 @@ class _GraphViewState extends State { ].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); + final s = await SharedPreferences.getInstance(); + chartType = s.getInt("monthlygraph") ?? 2; + setState(() {}); + }); } else if (value == AppLocalizations.of(context).about) { showAboutDialog( context: context, @@ -219,58 +269,10 @@ class _GraphViewState extends State { padding: const EdgeInsets.all(8), child: Column( children: [ - PlatformButton( - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.primary, - ), - foregroundColor: MaterialStateProperty.all( - Theme.of(context).colorScheme.onPrimary, - ), - ), - text: yearly - ? DateFormat.y(locale).format(_selectedDate) - : DateFormat.yMMMM(locale) - .format(_selectedDate), - onPressed: () async { - final firstDate = (selectedWallet!.entries - ..sort( - (a, b) => a.date.compareTo(b.date), - )) - .first - .date; - final lastDate = (selectedWallet!.entries - ..sort( - (a, b) => b.date.compareTo(a.date), - )) - .first - .date; - final newDate = await showDatePicker( - context: context, - initialDate: DateTime( - _selectedDate.year, - _selectedDate.month, - ), - firstDate: firstDate, - lastDate: lastDate, - initialEntryMode: yearly - ? DatePickerEntryMode.input - : DatePickerEntryMode.calendar, - initialDatePickerMode: yearly - ? DatePickerMode.year - : DatePickerMode.day, - ); - if (newDate == null) return; - _selectedDate = newDate; - setState(() {}); - }, - ), - const SizedBox( - height: 5, - ), SizedBox( width: MediaQuery.of(context).size.width * 0.9, - height: 300, + height: + MediaQuery.of(context).size.height * 0.35, child: (chartType == null) ? const CircularProgressIndicator() : (chartType == 1) @@ -292,29 +294,53 @@ class _GraphViewState extends State { ) : [], ) - : ExpensesLineChart( - currency: selectedWallet!.currency, - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: (graphTypeSet - .contains("expense")) - ? generateChartData( - EntryType.expense, - ) - : [], - incomeData: (graphTypeSet - .contains("income")) - ? generateChartData( - EntryType.income, - ) - : [], + : Padding( + padding: const EdgeInsets.all(8), + child: ExpensesLineChart( + currency: + selectedWallet!.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: (graphTypeSet + .contains("expense")) + ? generateChartData( + EntryType.expense, + ) + : [], + incomeData: (graphTypeSet + .contains("income")) + ? generateChartData( + EntryType.income, + ) + : [], + ), ), ), ], ), ), ), + const SizedBox( + height: 25, + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: + Theme.of(context).colorScheme.secondaryContainer, + ), + width: MediaQuery.of(context).size.width * 0.95, + height: MediaQuery.of(context).size.height * 0.35, + child: Padding( + padding: const EdgeInsets.all(8), + child: CategoriesPieChart( + symbol: selectedWallet!.currency.symbol, + entries: selectedWallet!.entries, + categories: selectedWallet!.categories, + ), + ), + ), ], ), ), diff --git a/lib/views/setup.dart b/lib/views/setup.dart index 320d12a..bc0b071 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -62,7 +62,7 @@ class _SetupViewState extends State { Icons.payments.codePoint, fontFamily: 'MaterialIcons', ), - color: Colors.transparent, + color: Theme.of(context).colorScheme.secondary, ), WalletCategory( name: AppLocalizations.of(context).categoryHealth, -- 2.45.2