diff --git a/CHANGELOG.md b/CHANGELOG.md index 5955607..a86776d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,5 @@ +# 1.0.0-alpha.2 +- Fixed localization issues +- Added graphs for expenses and income per month/year # 1.0.0-alpha - First public release \ No newline at end of file diff --git a/l10n.yaml b/l10n.yaml index 4e6692e..c114495 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -1,3 +1,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dart \ No newline at end of file +output-localization-file: app_localizations.dart +nullable-getter: false \ No newline at end of file diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb index 58a74db..566df2c 100644 --- a/lib/l10n/app_cs.arb +++ b/lib/l10n/app_cs.arb @@ -1,4 +1,5 @@ { + "@@locale": "cs", "categoryHealth": "Zdraví", "categoryCar": "Auto", "categoryFood": "Jídlo", @@ -48,9 +49,9 @@ "ocr": "OCR", "ocrData": "OCR Data", "downloaded": "Staženo", - "deleteOcr": "Opravdu chcete smazat '$lang' OCR data?\nJiž tato data nebudete moct použít při skenování obrázků.", - "langDownloadDialog": "Stahuji $lang, vyčkejte prosím...", - "langDownloadProgress": "Postup: $progress %", + "deleteOcr": "Opravdu chcete smazat '{lang}' OCR data?\nJiž tato data nebudete moct použít při skenování obrázků.", + "langDownloadDialog": "Stahuji {lang}, vyčkejte prosím...", + "langDownloadProgress": "Postup: {progress} %", "addingFromOcr": "Přidat skrz OCR", "license":"©️ 2023 Matyáš Caras\nVydáno pod licencí GNU AGPL license verze 3", "description":"Popis", @@ -61,6 +62,16 @@ "createTestData":"Vytvořit vzorková data", "yearly":"Roční", "monthly":"Měsíční", - "expenses":"Výdaje" + "expenses":"Výdaje", + "expensesForMonth": "Výdaje za {month}: {value}", + "incomeForMonth": "Příjmy za {month}: {value}", + "expensesForDay": "Výdaje: {value}", + "incomeForDay": "Příjmy: {value}", + "settingsAppearance":"Vzhled", + "graphType":"Druh grafu", + "graphTypeDesc":"Zvolte, zda-li použít sloupcový, nebo spojnicový graf, a kde", + "lineChart":"Spojnicový", + "barChart":"Sloupcový", + "selectType":"Zvolte typ" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1b49332..e0ab041 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,5 @@ { + "@@locale": "en", "categoryHealth": "Health", "categoryCar": "Car", "categoryFood": "Food", @@ -57,7 +58,7 @@ "ocr": "OCR", "ocrData": "OCR Data", "downloaded": "Downloaded", - "deleteOcr": "Do you really want to delete '$lang' OCR data?\nYou will not be able to use these language data when scanning pictures.", + "deleteOcr": "Do you really want to delete '{lang}' OCR data?\nYou will not be able to use these language data when scanning pictures.", "@deleteOcr": { "description": "Shown when a user wants to delete OCR data through settings", "placeholders": { @@ -67,7 +68,7 @@ } } }, - "langDownloadDialog": "Downloading $lang, please wait...", + "langDownloadDialog": "Downloading {lang}, please wait...", "@langDownloadDialog": { "description": "Shown as a title of a dialog while downloading new OCR data", "placeholders": { @@ -77,25 +78,79 @@ } } }, - "langDownloadProgress": "Download progress: $progress %", + "langDownloadProgress": "Download progress: {progress} %", "@langDownloadProgress": { "description": "Progress percentage shown while downloading OCR data", "placeholders": { - "progress":{ - "type":"num", - "example":"99.7" + "progress": { + "type": "num", + "example": "99.7" } } }, "addingFromOcr": "Add from OCR", - "license":"©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3", - "description":"Description", - "newWallet":"Add new wallet", - "walletExists":"A wallet with this name already exists!", - "setupStartingBalance":"Starting balance", - "graphs":"Graphs", - "createTestData":"Create test data", - "yearly":"Yearly", - "monthly":"Monthly", - "expenses":"Expenses" + "license": "©️ 2023 Matyáš Caras\nReleased under the GNU AGPL license version 3", + "description": "Description", + "newWallet": "Add new wallet", + "walletExists": "A wallet with this name already exists!", + "setupStartingBalance": "Starting balance", + "graphs": "Graphs", + "createTestData": "Create test data", + "yearly": "Yearly", + "monthly": "Monthly", + "expenses": "Expenses", + "expensesForMonth": "{month} expenses: {value}", + "@expensesForMonth": { + "description": "Shown as a tooltip when touching expense graph", + "placeholders": { + "month": { + "type": "String", + "example": "January" + }, + "value": { + "type": "String", + "example": "5000 Kč" + } + } + }, + "incomeForMonth": "{month} income: {value}", + "@incomeForMonth": { + "description": "Shown as a tooltip when touching income graph", + "placeholders": { + "month": { + "type": "String", + "example": "January" + }, + "value": { + "type": "String", + "example": "5000 Kč" + } + } + }, + "expensesForDay": "Expenses: {value}", + "@expensesForDay":{ + "description": "Shown as a tooltip when touching expense graph", + "placeholders": { + "value": { + "type": "String", + "example": "5000 Kč" + } + } + }, + "incomeForDay": "Income: {value}", + "@incomeForDay":{ + "description": "Shown as a tooltip when touching expense graph", + "placeholders": { + "value": { + "type": "String", + "example": "5000 Kč" + } + } + }, + "settingsAppearance":"Appearance", + "graphType":"Graph type", + "graphTypeDesc":"Choose whether to show line or bar chart and where", + "lineChart":"Line chart", + "barChart":"Bar chart", + "selectType":"Select type" } \ No newline at end of file diff --git a/lib/util/drawer.dart b/lib/util/drawer.dart index fbc3a20..c49d3ae 100644 --- a/lib/util/drawer.dart +++ b/lib/util/drawer.dart @@ -12,7 +12,7 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( ListTile( leading: const Icon(Icons.home), title: Text( - AppLocalizations.of(context)!.home, + AppLocalizations.of(context).home, ), selected: page == 1, onTap: () { @@ -27,7 +27,7 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer( ListTile( leading: const Icon(Icons.bar_chart), title: Text( - AppLocalizations.of(context)!.graphs, + AppLocalizations.of(context).graphs, ), selected: page == 2, onTap: () { diff --git a/lib/util/graphs.dart b/lib/util/graphs.dart index 5227e20..06f0305 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -1,21 +1,24 @@ +import 'package:currency_picker/currency_picker.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -import 'package:prasule/main.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Monthly/Yearly expense/income [LineChart] -class ExpensesChart extends StatelessWidget { - const ExpensesChart( +class ExpensesLineChart extends StatelessWidget { + const ExpensesLineChart( {super.key, required this.date, required this.locale, - this.expenseData = const [], - this.incomeData = const [], + required this.expenseData, + required this.incomeData, + required this.currency, this.yearly = false}); final bool yearly; final DateTime date; final String locale; final List expenseData; + final Currency currency; List get expenseDataSorted { var list = List.from(expenseData); list.sort((a, b) => a.compareTo(b)); @@ -43,10 +46,36 @@ class ExpensesChart extends StatelessWidget { Widget build(BuildContext context) { return LineChart( LineChartData( - maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (spots) => List.generate( + spots.length, + (index) => LineTooltipItem( + (yearly) + ? AppLocalizations.of(context).expensesForMonth( + DateFormat.MMMM(locale).format( + DateTime(date.year, spots[index].x.toInt() + 1, 1)), + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(spots[index].y)) + : AppLocalizations.of(context).expensesForDay( + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(spots[index].y), + ), + TextStyle(color: spots[index].bar.color), + ), + ), + ), + ), maxY: maxY, - minX: 1, + maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), minY: 0, + minX: 0, backgroundColor: Theme.of(context).colorScheme.background, lineBarsData: [ if (incomeData.isNotEmpty) @@ -59,7 +88,7 @@ class ExpensesChart extends StatelessWidget { color: Theme.of(context).colorScheme.primary, spots: List.generate( (yearly) ? 12 : DateTime(date.year, date.month, 0).day, - (index) => FlSpot(index.toDouble() + 1, incomeData[index]), + (index) => FlSpot(index.toDouble(), incomeData[index]), ), ), if (expenseData.isNotEmpty) @@ -85,15 +114,16 @@ class ExpensesChart extends StatelessWidget { ), bottomTitles: AxisTitles( sideTitles: SideTitles( + reservedSize: 30, showTitles: true, getTitlesWidget: (value, meta) { String text; if (yearly) { text = DateFormat.MMM(locale).format( - DateTime(date.year, value.toInt(), 1), + DateTime(date.year, value.toInt() + 1, 1), ); } else { - text = value.toInt().toString(); + text = (value.toInt() + 1).toString(); } return SideTitleWidget( axisSide: meta.axisSide, child: Text(text)); @@ -105,3 +135,141 @@ class ExpensesChart extends StatelessWidget { ); } } + +class ExpensesBarChart extends StatelessWidget { + const ExpensesBarChart( + {super.key, + required this.yearly, + required this.date, + required this.locale, + required this.expenseData, + required this.incomeData, + required this.currency}); + final bool yearly; + final DateTime date; + final String locale; + final List expenseData; + List get expenseDataSorted { + var list = List.from(expenseData); + list.sort((a, b) => a.compareTo(b)); + return list; + } + + final Currency currency; + + final List incomeData; + List get incomeDataSorted { + var list = List.from(incomeData); + list.sort((a, b) => a.compareTo(b)); + return list; + } + + double get maxY { + if (incomeData.isEmpty) return expenseDataSorted.last; + if (expenseData.isEmpty) return incomeDataSorted.last; + if (expenseDataSorted.last > incomeDataSorted.last) { + return expenseDataSorted.last; + } else { + return incomeDataSorted.last; + } + } + + @override + Widget build(BuildContext context) => BarChart( + BarChartData( + barTouchData: BarTouchData( + enabled: true, + touchTooltipData: BarTouchTooltipData( + getTooltipItem: (group, groupIndex, rod, rodIndex) => + (yearly) // create custom tooltips for graph bars + ? BarTooltipItem( + (rodIndex == 1) + ? AppLocalizations.of(context).expensesForMonth( + DateFormat.MMMM(locale).format( + DateTime(date.year, groupIndex + 1, 1)), + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(rod.toY), + ) + : AppLocalizations.of(context).incomeForMonth( + DateFormat.MMMM(locale).format( + DateTime(date.year, groupIndex + 1, 1)), + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(rod.toY), + ), + TextStyle(color: rod.color), + ) + : BarTooltipItem( + (rodIndex == 1) + ? AppLocalizations.of(context).expensesForDay( + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(rod.toY), + ) + : AppLocalizations.of(context).incomeForDay( + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(rod.toY), + ), + TextStyle(color: rod.color), + ), + ), + ), + titlesData: FlTitlesData( + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 30, + getTitlesWidget: (value, meta) { + String text; + if (yearly) { + text = DateFormat.MMM(locale).format( + DateTime(date.year, value.toInt() + 1, 1), + ); + } else { + text = (value.toInt() + 1).toString(); + } + return SideTitleWidget( + axisSide: meta.axisSide, child: Text(text)); + }, + ), + ), + ), // axis descriptions, + minY: 0, + maxY: maxY, + barGroups: List.generate( + (yearly) ? 12 : DateTime(date.year, date.month, 0).day, + (index) => BarChartGroupData( + x: index, + barRods: [ + if (incomeData.isNotEmpty) + BarChartRodData( + toY: incomeData[index], + color: Theme.of(context).colorScheme.primary, + ), + if (expenseData.isNotEmpty) + BarChartRodData( + toY: expenseData[index], + color: Theme.of(context).colorScheme.error, + ), + ], + ), + ), + ), + ); +} diff --git a/lib/views/create_entry.dart b/lib/views/create_entry.dart index 9a8298c..57f2541 100644 --- a/lib/views/create_entry.dart +++ b/lib/views/create_entry.dart @@ -40,7 +40,7 @@ class _CreateEntryViewState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(AppLocalizations.of(context)!.createEntry), + title: Text(AppLocalizations.of(context).createEntry), ), body: SizedBox( width: MediaQuery.of(context).size.width, @@ -53,7 +53,7 @@ class _CreateEntryViewState extends State { SizedBox( width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( - labelText: AppLocalizations.of(context)!.name, + labelText: AppLocalizations.of(context).name, controller: TextEditingController(text: newEntry.data.name), onChanged: (v) { newEntry.data.name = v; @@ -66,7 +66,7 @@ class _CreateEntryViewState extends State { SizedBox( width: MediaQuery.of(context).size.width * 0.8, child: PlatformField( - labelText: AppLocalizations.of(context)!.amount, + labelText: AppLocalizations.of(context).amount, controller: TextEditingController( text: newEntry.data.amount.toString()), keyboardType: @@ -83,7 +83,7 @@ class _CreateEntryViewState extends State { const SizedBox( height: 20, ), - Text(AppLocalizations.of(context)!.type), + Text(AppLocalizations.of(context).type), const SizedBox( height: 10, ), @@ -97,7 +97,7 @@ class _CreateEntryViewState extends State { child: SizedBox( width: MediaQuery.of(context).size.width * 0.8 - 24, child: Text( - AppLocalizations.of(context)!.expense, + AppLocalizations.of(context).expense, ), ), ), @@ -105,7 +105,7 @@ class _CreateEntryViewState extends State { value: EntryType.income, child: SizedBox( width: MediaQuery.of(context).size.width * 0.8 - 24, - child: Text(AppLocalizations.of(context)!.income), + child: Text(AppLocalizations.of(context).income), ), ), ], @@ -119,7 +119,7 @@ class _CreateEntryViewState extends State { const SizedBox( height: 20, ), - Text(AppLocalizations.of(context)!.category), + Text(AppLocalizations.of(context).category), const SizedBox( height: 10, ), @@ -151,7 +151,7 @@ class _CreateEntryViewState extends State { const SizedBox( height: 20, ), - Text(AppLocalizations.of(context)!.description), + Text(AppLocalizations.of(context).description), const SizedBox( height: 10, ), @@ -175,14 +175,14 @@ class _CreateEntryViewState extends State { height: 15, ), PlatformButton( - text: AppLocalizations.of(context)!.save, + text: AppLocalizations.of(context).save, onPressed: () { if (newEntry.data.name.isEmpty) { ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - AppLocalizations.of(context)!.errorEmptyName), + AppLocalizations.of(context).errorEmptyName), ), ); return; diff --git a/lib/views/graph_view.dart b/lib/views/graph_view.dart index ed69922..80ed028 100644 --- a/lib/views/graph_view.dart +++ b/lib/views/graph_view.dart @@ -1,4 +1,3 @@ -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:prasule/api/category.dart'; @@ -12,6 +11,7 @@ import 'package:prasule/util/graphs.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/setup.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class GraphView extends StatefulWidget { const GraphView({super.key}); @@ -71,10 +71,15 @@ class _GraphViewState extends State { setState(() {}); } + int? chartType; @override void initState() { super.initState(); loadWallet(); + SharedPreferences.getInstance().then((s) { + chartType = s.getInt("monthlygraph") ?? 2; + setState(() {}); + }); } @override @@ -95,7 +100,7 @@ class _GraphViewState extends State { ), DropdownMenuItem( value: -1, - child: Text(AppLocalizations.of(context)!.newWallet), + child: Text(AppLocalizations.of(context).newWallet), ) ], onChanged: (v) async { @@ -120,20 +125,20 @@ class _GraphViewState extends State { actions: [ PopupMenuButton( itemBuilder: (context) => [ - AppLocalizations.of(context)!.settings, - AppLocalizations.of(context)!.about + AppLocalizations.of(context).settings, + AppLocalizations.of(context).about ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), onSelected: (value) { - if (value == AppLocalizations.of(context)!.settings) { + if (value == AppLocalizations.of(context).settings) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SettingsView(), ), ); - } else if (value == AppLocalizations.of(context)!.about) { + } else if (value == AppLocalizations.of(context).about) { showAboutDialog( context: context, - applicationLegalese: AppLocalizations.of(context)!.license, + applicationLegalese: AppLocalizations.of(context).license, applicationName: "Prašule"); } }, @@ -143,103 +148,153 @@ class _GraphViewState extends State { drawer: makeDrawer(context, 2), body: SingleChildScrollView( child: Center( - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SegmentedButton( - segments: [ - ButtonSegment( - value: "expense", - label: Text(AppLocalizations.of(context)!.expenses), - ), - ButtonSegment( - value: "income", - label: Text(AppLocalizations.of(context)!.income), - ), - ], - selected: graphTypeSet, - multiSelectionEnabled: true, - onSelectionChanged: (selection) { - graphTypeSet = selection; - setState(() {}); - }, - ), - const SizedBox( - height: 5, - ), - SegmentedButton( - segments: [ - ButtonSegment( - value: "yearly", - label: Text(AppLocalizations.of(context)!.yearly), - ), - ButtonSegment( - value: "monthly", - label: Text(AppLocalizations.of(context)!.monthly), - ), - ], - selected: yearlyBtnSet, - onSelectionChanged: (selection) { - yearlyBtnSet = selection; - setState(() {}); - }, - ), - const SizedBox(height: 5), - PlatformButton( - text: (yearly) - ? DateFormat.y(locale).format(_selectedDate) - : DateFormat.yMMMM(locale).format(_selectedDate), - onPressed: () async { - var firstDate = (selectedWallet!.entries - ..sort((a, b) => a.date.compareTo(b.date))) - .first - .date; - var lastDate = (selectedWallet!.entries - ..sort((a, b) => b.date.compareTo(a.date))) - .first - .date; - logger.i(firstDate); - logger.i(lastDate); - var newDate = await showDatePicker( - context: context, - initialDate: _selectedDate, - 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, - child: ExpensesChart( - date: _selectedDate, - locale: locale ?? "en", - yearly: yearly, - expenseData: (graphTypeSet.contains("expense")) - ? generateChartData(EntryType.expense) - : [], - incomeData: (graphTypeSet.contains("income")) - ? generateChartData(EntryType.income) - : [], - ), + child: (selectedWallet == null) + ? const CircularProgressIndicator( + strokeWidth: 5, ) - ], - ), - ), + : SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SegmentedButton( + segments: [ + ButtonSegment( + value: "expense", + label: Text(AppLocalizations.of(context).expenses), + ), + ButtonSegment( + value: "income", + label: Text(AppLocalizations.of(context).income), + ), + ], + selected: graphTypeSet, + multiSelectionEnabled: true, + onSelectionChanged: (selection) { + graphTypeSet = selection; + setState(() {}); + }, + ), + const SizedBox( + height: 5, + ), + SegmentedButton( + segments: [ + ButtonSegment( + value: "yearly", + label: Text(AppLocalizations.of(context).yearly), + ), + ButtonSegment( + value: "monthly", + label: Text(AppLocalizations.of(context).monthly), + ), + ], + selected: yearlyBtnSet, + onSelectionChanged: (selection) async { + yearlyBtnSet = selection; + var s = await SharedPreferences.getInstance(); + chartType = (yearly) + ? (s.getInt("yearlygraph") ?? 1) + : (s.getInt("monthlygraph") ?? 2); + setState(() {}); + }, + ), + const SizedBox(height: 5), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .secondaryContainer), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + PlatformButton( + text: (yearly) + ? DateFormat.y(locale).format(_selectedDate) + : DateFormat.yMMMM(locale) + .format(_selectedDate), + onPressed: () async { + var firstDate = (selectedWallet!.entries + ..sort( + (a, b) => a.date.compareTo(b.date))) + .first + .date; + var lastDate = (selectedWallet!.entries + ..sort( + (a, b) => b.date.compareTo(a.date))) + .first + .date; + logger.i(firstDate); + logger.i(lastDate); + var newDate = await showDatePicker( + context: context, + initialDate: DateTime(_selectedDate.year, + _selectedDate.month, 1), + 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, + child: (chartType == null) + ? const CircularProgressIndicator() + : (chartType == 1) + ? ExpensesBarChart( + currency: selectedWallet!.currency, + date: _selectedDate, + locale: locale ?? "en", + yearly: yearly, + expenseData: (graphTypeSet + .contains("expense")) + ? generateChartData( + EntryType.expense) + : [], + incomeData: (graphTypeSet + .contains("income")) + ? generateChartData( + EntryType.income) + : [], + ) + : 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) + : [], + ), + ) + ], + ), + ), + ) + ], + ), + ), ), ), ); diff --git a/lib/views/home.dart b/lib/views/home.dart index cf38e6f..23b8dc7 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -72,7 +72,7 @@ class _HomeViewState extends State { if (kDebugMode) SpeedDialChild( child: const Icon(Icons.bug_report), - label: AppLocalizations.of(context)!.createTestData, + label: AppLocalizations.of(context).createTestData, onTap: () { // debug option to quickly fill a wallet with data if (selectedWallet == null) return; @@ -114,7 +114,7 @@ class _HomeViewState extends State { ), SpeedDialChild( child: const Icon(Icons.edit), - label: AppLocalizations.of(context)!.addNew, + label: AppLocalizations.of(context).addNew, onTap: () async { var sw = await Navigator.of(context).push( MaterialPageRoute( @@ -128,7 +128,7 @@ class _HomeViewState extends State { }), SpeedDialChild( child: const Icon(Icons.camera_alt), - label: AppLocalizations.of(context)!.addCamera, + label: AppLocalizations.of(context).addCamera, onTap: () async { final ImagePicker picker = ImagePicker(); final XFile? media = @@ -138,7 +138,7 @@ class _HomeViewState extends State { ), SpeedDialChild( child: const Icon(Icons.image), - label: AppLocalizations.of(context)!.addGallery, + label: AppLocalizations.of(context).addGallery, onTap: () { startOcr(ImageSource.gallery); }, @@ -160,7 +160,7 @@ class _HomeViewState extends State { ), DropdownMenuItem( value: -1, - child: Text(AppLocalizations.of(context)!.newWallet), + child: Text(AppLocalizations.of(context).newWallet), ) ], onChanged: (v) async { @@ -185,20 +185,20 @@ class _HomeViewState extends State { actions: [ PopupMenuButton( itemBuilder: (context) => [ - AppLocalizations.of(context)!.settings, - AppLocalizations.of(context)!.about + AppLocalizations.of(context).settings, + AppLocalizations.of(context).about ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), onSelected: (value) { - if (value == AppLocalizations.of(context)!.settings) { + if (value == AppLocalizations.of(context).settings) { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const SettingsView(), ), ); - } else if (value == AppLocalizations.of(context)!.about) { + } else if (value == AppLocalizations.of(context).about) { showAboutDialog( context: context, - applicationLegalese: AppLocalizations.of(context)!.license, + applicationLegalese: AppLocalizations.of(context).license, applicationName: "Prašule"); } }, @@ -223,14 +223,14 @@ class _HomeViewState extends State { ? Column( children: [ Text( - AppLocalizations.of(context)!.noEntries, + AppLocalizations.of(context).noEntries, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, ), ), Text( - AppLocalizations.of(context)!.noEntriesSub, + AppLocalizations.of(context).noEntriesSub, ) ], ) @@ -307,12 +307,12 @@ class _HomeViewState extends State { context: context, builder: (cx) => PlatformDialog( title: - AppLocalizations.of(context)!.sureDialog, + AppLocalizations.of(context).sureDialog, content: Text( - AppLocalizations.of(context)!.deleteSure), + AppLocalizations.of(context).deleteSure), actions: [ PlatformButton( - text: AppLocalizations.of(context)!.yes, + text: AppLocalizations.of(context).yes, onPressed: () { selectedWallet?.entries.removeWhere( (e) => e.id == element.id); @@ -323,7 +323,7 @@ class _HomeViewState extends State { }, ), PlatformButton( - text: AppLocalizations.of(context)!.no, + text: AppLocalizations.of(context).no, onPressed: () { Navigator.of(cx).pop(); }, @@ -365,9 +365,9 @@ class _HomeViewState extends State { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(AppLocalizations.of(context)!.missingOcr), + content: Text(AppLocalizations.of(context).missingOcr), action: SnackBarAction( - label: AppLocalizations.of(context)!.download, + label: AppLocalizations.of(context).download, onPressed: () { Navigator.of(context).push( MaterialPageRoute( @@ -408,7 +408,7 @@ class _HomeViewState extends State { showDialog( context: context, builder: (c) => PlatformDialog( - title: AppLocalizations.of(context)!.ocrLoading), + title: AppLocalizations.of(context).ocrLoading), barrierDismissible: false); var string = await FlutterTesseractOcr.extractText(media.path, language: selected, @@ -472,7 +472,7 @@ class _HomeViewState extends State { child: const Text("Cancel"), ), ], - title: AppLocalizations.of(context)!.ocrSelect, + title: AppLocalizations.of(context).ocrSelect, content: Column( children: [ ...List.generate( diff --git a/lib/views/settings/graph_type.dart b/lib/views/settings/graph_type.dart new file mode 100644 index 0000000..9b5eb5f --- /dev/null +++ b/lib/views/settings/graph_type.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:prasule/pw/platformdialog.dart'; +import 'package:settings_ui/settings_ui.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class GraphTypeSettingsView extends StatefulWidget { + const GraphTypeSettingsView({super.key}); + + @override + State createState() => _GraphTypeSettingsViewState(); +} + +class _GraphTypeSettingsViewState extends State { + var _yearly = 1; + var _monthly = 2; + @override + void initState() { + super.initState(); + SharedPreferences.getInstance().then((prefs) { + _yearly = prefs.getInt("yearlygraph") ?? 1; + _monthly = prefs.getInt("monthlygraph") ?? 2; + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context).graphType), + ), + body: SettingsList( + applicationType: ApplicationType.both, + darkTheme: SettingsThemeData( + settingsListBackground: Theme.of(context).colorScheme.background, + titleTextColor: Theme.of(context).colorScheme.primary), + sections: [ + SettingsSection( + tiles: [ + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).yearly), + value: Text(_yearly == 1 + ? AppLocalizations.of(context).barChart + : AppLocalizations.of(context).lineChart), + onPressed: (c) => showDialog( + context: c, + builder: (ctx) => PlatformDialog( + title: AppLocalizations.of(context).selectType, + content: Column( + children: [ + SizedBox( + width: MediaQuery.of(ctx).size.width, + child: InkWell( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(AppLocalizations.of(context).barChart, + textAlign: TextAlign.center), + ), + onTap: () async { + var s = await SharedPreferences.getInstance(); + s.setInt("yearlygraph", 1); + _yearly = 1; + if (!mounted) return; + Navigator.of(ctx).pop(); + setState(() {}); + }, + ), + ), + SizedBox( + width: MediaQuery.of(context).size.width, + child: InkWell( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + AppLocalizations.of(context).lineChart, + textAlign: TextAlign.center, + ), + ), + onTap: () async { + var s = await SharedPreferences.getInstance(); + s.setInt("yearlygraph", 2); + _yearly = 2; + if (!mounted) return; + Navigator.of(ctx).pop(); + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ), + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).monthly), + value: Text(_monthly == 1 + ? AppLocalizations.of(context).barChart + : AppLocalizations.of(context).lineChart), + onPressed: (c) => showDialog( + context: c, + builder: (ctx) => PlatformDialog( + title: AppLocalizations.of(context).selectType, + content: Column( + children: [ + SizedBox( + width: MediaQuery.of(ctx).size.width, + child: InkWell( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + AppLocalizations.of(context).barChart, + textAlign: TextAlign.center, + ), + ), + onTap: () async { + var s = await SharedPreferences.getInstance(); + s.setInt("monthlygraph", 1); + _monthly = 1; + if (!mounted) return; + Navigator.of(ctx).pop(); + setState(() {}); + }, + ), + ), + SizedBox( + width: MediaQuery.of(ctx).size.width, + child: InkWell( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + AppLocalizations.of(context).lineChart, + textAlign: TextAlign.center), + ), + onTap: () async { + var s = await SharedPreferences.getInstance(); + s.setInt("monthlygraph", 2); + _monthly = 2; + if (!mounted) return; + Navigator.of(ctx).pop(); + setState(() {}); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/views/settings/settings.dart b/lib/views/settings/settings.dart index 7bd4a31..6bd5e63 100644 --- a/lib/views/settings/settings.dart +++ b/lib/views/settings/settings.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:prasule/pw/platformroute.dart'; +import 'package:prasule/views/settings/graph_type.dart'; import 'package:prasule/views/settings/tessdata_list.dart'; import 'package:settings_ui/settings_ui.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -14,7 +16,7 @@ class _SettingsViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), + appBar: AppBar(title: Text(AppLocalizations.of(context).settings)), body: SettingsList( applicationType: ApplicationType.both, darkTheme: SettingsThemeData( @@ -24,16 +26,31 @@ class _SettingsViewState extends State { SettingsSection( tiles: [ SettingsTile.navigation( - title: Text(AppLocalizations.of(context)!.downloadedOcr), + title: Text(AppLocalizations.of(context).downloadedOcr), description: - Text(AppLocalizations.of(context)!.downloadedOcrDesc), - onPressed: (context) => Navigator.of(context).push( - MaterialPageRoute( - builder: (c) => const TessdataListView())), + Text(AppLocalizations.of(context).downloadedOcrDesc), + trailing: const Icon(Icons.keyboard_arrow_right), + onPressed: (context) => Navigator.of(context) + .push(platformRoute((c) => const TessdataListView())), ) ], - title: Text(AppLocalizations.of(context)!.ocr), + title: Text(AppLocalizations.of(context).ocr), ), + SettingsSection( + title: Text(AppLocalizations.of(context).settingsAppearance), + tiles: [ + SettingsTile.navigation( + title: Text(AppLocalizations.of(context).graphType), + description: Text(AppLocalizations.of(context).graphTypeDesc), + trailing: const Icon(Icons.keyboard_arrow_right), + onPressed: (c) => Navigator.of(c).push( + platformRoute( + (p0) => const GraphTypeSettingsView(), + ), + ), + ) + ], + ) ], ), ); diff --git a/lib/views/settings/tessdata_list.dart b/lib/views/settings/tessdata_list.dart index 472eec1..6fcee0c 100644 --- a/lib/views/settings/tessdata_list.dart +++ b/lib/views/settings/tessdata_list.dart @@ -29,7 +29,7 @@ class _TessdataListViewState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context)!.ocrData)), + appBar: AppBar(title: Text(AppLocalizations.of(context).ocrData)), body: Center( child: SizedBox( width: MediaQuery.of(context).size.width * 0.9, @@ -50,8 +50,8 @@ class _TessdataListViewState extends State { title: Text(_tessdata[i].keys.first), trailing: TextButton( child: Text(_tessdata[i][_tessdata[i].keys.first]! - ? AppLocalizations.of(context)!.downloaded - : AppLocalizations.of(context)!.download), + ? AppLocalizations.of(context).downloaded + : AppLocalizations.of(context).download), onPressed: () async { var lang = _tessdata[i].keys.first; if (_tessdata[i][lang]!) { @@ -59,12 +59,12 @@ class _TessdataListViewState extends State { showDialog( context: context, builder: (context) => PlatformDialog( - title: AppLocalizations.of(context)!.sureDialog, - content: Text(AppLocalizations.of(context)! + title: AppLocalizations.of(context).sureDialog, + content: Text(AppLocalizations.of(context) .deleteOcr(lang)), actions: [ PlatformButton( - text: AppLocalizations.of(context)!.yes, + text: AppLocalizations.of(context).yes, onPressed: () async { await TessdataApi.deleteData(lang); _tessdata[i][lang] = true; @@ -72,7 +72,7 @@ class _TessdataListViewState extends State { }, ), PlatformButton( - text: AppLocalizations.of(context)!.no, + text: AppLocalizations.of(context).no, onPressed: () { Navigator.of(context).pop(); }, @@ -91,7 +91,7 @@ class _TessdataListViewState extends State { showDialog( context: context, builder: (c) => PlatformDialog( - title: AppLocalizations.of(context)! + title: AppLocalizations.of(context) .langDownloadDialog(lang), content: StreamBuilder( builder: (context, snapshot) { @@ -102,7 +102,7 @@ class _TessdataListViewState extends State { if (snapshot.hasError) { return const Text("Error"); } - return Text(AppLocalizations.of(context)! + return Text(AppLocalizations.of(context) .langDownloadProgress(snapshot.data!)); }, stream: progressStream.stream, diff --git a/lib/views/setup.dart b/lib/views/setup.dart index 24dfbd7..9cebdbc 100644 --- a/lib/views/setup.dart +++ b/lib/views/setup.dart @@ -45,28 +45,28 @@ class _SetupViewState extends State { if (categories.isEmpty) { categories = [ WalletCategory( - name: AppLocalizations.of(context)!.categoryHealth, + name: AppLocalizations.of(context).categoryHealth, type: EntryType.expense, id: 1, icon: IconData(Icons.medical_information.codePoint, fontFamily: 'MaterialIcons'), ), WalletCategory( - name: AppLocalizations.of(context)!.categoryCar, + name: AppLocalizations.of(context).categoryCar, type: EntryType.expense, id: 2, icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), ), WalletCategory( - name: AppLocalizations.of(context)!.categoryFood, + name: AppLocalizations.of(context).categoryFood, type: EntryType.expense, id: 3, icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), ), WalletCategory( - name: AppLocalizations.of(context)!.categoryTravel, + name: AppLocalizations.of(context).categoryTravel, type: EntryType.expense, id: 4, icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), @@ -90,16 +90,16 @@ class _SetupViewState extends State { showNextButton: true, showBackButton: true, showDoneButton: true, - next: Text(AppLocalizations.of(context)!.next), - back: Text(AppLocalizations.of(context)!.back), - done: Text(AppLocalizations.of(context)!.finish), + next: Text(AppLocalizations.of(context).next), + back: Text(AppLocalizations.of(context).back), + done: Text(AppLocalizations.of(context).finish), onDone: () { if (name.isEmpty) { ScaffoldMessenger.of(context) .clearSnackBars(); // TODO: iOS replacement ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: - Text(AppLocalizations.of(context)!.errorEmptyName))); + Text(AppLocalizations.of(context).errorEmptyName))); return; } var wallet = Wallet( @@ -113,7 +113,7 @@ class _SetupViewState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: - Text(AppLocalizations.of(context)!.walletExists), + Text(AppLocalizations.of(context).walletExists), ), ); return; @@ -137,7 +137,7 @@ class _SetupViewState extends State { titleWidget: Padding( padding: const EdgeInsets.all(8), child: Text( - AppLocalizations.of(context)!.welcome, + AppLocalizations.of(context).welcome, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center, @@ -150,7 +150,7 @@ class _SetupViewState extends State { if (!widget.newWallet) Flexible( child: Text( - AppLocalizations.of(context)!.welcomeAboutPrasule), + AppLocalizations.of(context).welcomeAboutPrasule), ), if (!widget.newWallet) const SizedBox( @@ -158,7 +158,7 @@ class _SetupViewState extends State { ), Flexible( child: Text( - AppLocalizations.of(context)!.welcomeInstruction), + AppLocalizations.of(context).welcomeInstruction), ), ], ), @@ -169,7 +169,7 @@ class _SetupViewState extends State { titleWidget: Padding( padding: const EdgeInsets.all(8), child: Text( - AppLocalizations.of(context)!.setupWalletNameCurrency, + AppLocalizations.of(context).setupWalletNameCurrency, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold), @@ -182,7 +182,7 @@ class _SetupViewState extends State { width: MediaQuery.of(context).size.width * 0.7, child: PlatformField( labelText: - AppLocalizations.of(context)!.setupNamePlaceholder, + AppLocalizations.of(context).setupNamePlaceholder, onChanged: (t) { name = t; }, @@ -192,7 +192,7 @@ class _SetupViewState extends State { height: 5, ), PlatformButton( - text: AppLocalizations.of(context)! + text: AppLocalizations.of(context) .setupCurrency(_selectedCurrency.code), onPressed: () { showCurrencyPicker( @@ -211,7 +211,7 @@ class _SetupViewState extends State { width: MediaQuery.of(context).size.width * 0.7, child: PlatformField( labelText: - AppLocalizations.of(context)!.setupStartingBalance, + AppLocalizations.of(context).setupStartingBalance, keyboardType: const TextInputType.numberWithOptions( decimal: true), inputFormatters: [ @@ -233,7 +233,7 @@ class _SetupViewState extends State { titleWidget: Padding( padding: const EdgeInsets.all(8), child: Text( - AppLocalizations.of(context)!.setupCategoriesHeading, + AppLocalizations.of(context).setupCategoriesHeading, textAlign: TextAlign.center, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold), @@ -243,7 +243,7 @@ class _SetupViewState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.setupCategoriesEditHint, + AppLocalizations.of(context).setupCategoriesEditHint, textAlign: TextAlign.center, ), SizedBox( @@ -296,17 +296,17 @@ class _SetupViewState extends State { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.ok), + AppLocalizations.of(context).ok), ), TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text( - AppLocalizations.of(context)!.cancel), + AppLocalizations.of(context).cancel), ), ], - title: AppLocalizations.of(context)! + title: AppLocalizations.of(context) .setupCategoriesEditingName, content: SizedBox( width: 400, @@ -336,7 +336,7 @@ class _SetupViewState extends State { } categories.add( WalletCategory( - name: AppLocalizations.of(context)! + name: AppLocalizations.of(context) .setupWalletNamePlaceholder, type: EntryType.expense, id: id, diff --git a/pubspec.lock b/pubspec.lock index b4e2c3c..d8bb9d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -876,6 +876,62 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: @@ -1131,4 +1187,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.2.0 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.16.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8f0235b..d642b16 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: prasule description: Open-source private expense tracker -version: 1.0.0-alpha2+2 +version: 1.0.0-alpha.2+2 environment: sdk: '>=3.1.0-262.2.beta <4.0.0' @@ -39,6 +39,7 @@ dependencies: flutter_localizations: sdk: flutter fl_chart: ^0.65.0 + shared_preferences: ^2.2.2 dev_dependencies: flutter_test: