From a37e73f06634d6d2ad88110b81485968335f66a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Mon, 25 Dec 2023 21:43:25 +0100 Subject: [PATCH] feat: add income/expense graphs Reviewed-on: https://git.mnau.xyz/hernik/prasule/pulls/16 --- .flutter | 2 +- .vscode/settings.json | 3 +- CHANGELOG.md | 3 + .../app/FlutterMultiDexApplication.java | 25 ++ .../gradle/wrapper/gradle-wrapper.properties | 4 +- l10n.yaml | 3 +- lib/l10n/app_cs.arb | 23 +- lib/l10n/app_en.arb | 89 +++++- lib/main.dart | 51 ++-- lib/pw/platformbutton.dart | 10 +- lib/util/drawer.dart | 4 +- lib/util/graphs.dart | 273 ++++++++++++++++-- lib/views/create_entry.dart | 20 +- lib/views/graph_view.dart | 273 ++++++++++++------ lib/views/home.dart | 46 +-- lib/views/settings/graph_type.dart | 156 ++++++++++ lib/views/settings/settings.dart | 58 +++- lib/views/settings/tessdata_list.dart | 18 +- lib/views/setup.dart | 44 +-- pubspec.lock | 106 +++++-- pubspec.yaml | 3 +- 21 files changed, 947 insertions(+), 267 deletions(-) create mode 100644 android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java create mode 100644 lib/views/settings/graph_type.dart diff --git a/.flutter b/.flutter index 593a031..fed06b3 160000 --- a/.flutter +++ b/.flutter @@ -1 +1 @@ -Subproject commit 593a031efb1e54aef4d083fc810482c2877ba1d2 +Subproject commit fed06b31d938f7620ea7417295b8d8d19cf7cf1d diff --git a/.vscode/settings.json b/.vscode/settings.json index 4397789..9aa5bb4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "conventionalCommits.scopes": [ "ocr", "ui", - "translations" + "translations", + "graphs" ] } \ No newline at end of file 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/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java new file mode 100644 index 0000000..752fc18 --- /dev/null +++ b/android/app/src/main/java/io/flutter/app/FlutterMultiDexApplication.java @@ -0,0 +1,25 @@ +// Generated file. +// +// If you wish to remove Flutter's multidex support, delete this entire file. +// +// Modifications to this file should be done in a copy under a different name +// as this file may be regenerated. + +package io.flutter.app; + +import android.app.Application; +import android.content.Context; +import androidx.annotation.CallSuper; +import androidx.multidex.MultiDex; + +/** + * Extension of {@link android.app.Application}, adding multidex support. + */ +public class FlutterMultiDexApplication extends Application { + @Override + @CallSuper + protected void attachBaseContext(Context base) { + super.attachBaseContext(base); + MultiDex.install(this); + } +} diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c472b9..5c6f89d 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 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 3f32913..fc53ec4 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", @@ -59,8 +60,20 @@ "setupStartingBalance":"Počáteční zůstatek", "graphs":"Grafy", "createTestData":"Vytvořit vzorková data", - "spendingStats":"Statistiky utrácení", "yearly":"Roční", - "monthly":"Měsíční" + "monthly":"Měsíční", + "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", + "enableYou":"Povolit Material You (Může vyžadovat restart aplikace)", + "enableYouDesc":"Aplikace použije barevné schéma z vaší tapety" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3fd1003..875aeaa 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,81 @@ } } }, - "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", - "spendingStats":"Spending statistics", - "yearly":"Yearly", - "monthly":"Monthly" + "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", + "enableYou":"Enable Material You (May require an app restart)", + "enableYouDesc":"The app will use a color scheme from your wallpaper" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 5ccc47b..a093132 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,8 +8,13 @@ import 'package:logger/logger.dart'; import 'package:prasule/util/color_schemes.g.dart'; import 'package:prasule/views/home.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -void main() { +var _materialYou = false; +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + var s = await SharedPreferences.getInstance(); + _materialYou = s.getBool("useMaterialYou") ?? true; runApp(const MyApp()); } @@ -17,29 +22,37 @@ final logger = Logger(); class MyApp extends StatelessWidget { const MyApp({super.key}); - + static bool appliedYou = false; // This widget is the root of your application. @override Widget build(BuildContext context) { return (Platform.isAndroid) ? DynamicColorBuilder( - builder: (light, dark) => MaterialApp( - debugShowCheckedModeBanner: false, - localizationsDelegates: const [ - AppLocalizations.delegate, - ...GlobalMaterialLocalizations.delegates, - ...GlobalCupertinoLocalizations.delegates - ], - supportedLocales: AppLocalizations.supportedLocales, - title: 'Prašule', - theme: ThemeData( - colorScheme: light ?? lightColorScheme, - useMaterial3: true, - ), - darkTheme: ThemeData( - useMaterial3: true, colorScheme: dark ?? darkColorScheme), - home: const HomeView(), - ), + builder: (light, dark) { + appliedYou = light != null; + return MaterialApp( + debugShowCheckedModeBanner: false, + localizationsDelegates: const [ + AppLocalizations.delegate, + ...GlobalMaterialLocalizations.delegates, + ...GlobalCupertinoLocalizations.delegates + ], + supportedLocales: AppLocalizations.supportedLocales, + title: 'Prašule', + theme: ThemeData( + colorScheme: (_materialYou) + ? light ?? lightColorScheme + : lightColorScheme, + useMaterial3: true, + ), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: (_materialYou) + ? dark ?? darkColorScheme + : darkColorScheme), + home: const HomeView(), + ); + }, ) : Theme( data: ThemeData( diff --git a/lib/pw/platformbutton.dart b/lib/pw/platformbutton.dart index 901101d..aa49d08 100644 --- a/lib/pw/platformbutton.dart +++ b/lib/pw/platformbutton.dart @@ -5,12 +5,16 @@ import 'package:prasule/pw/platformwidget.dart'; class PlatformButton extends PlatformWidget { final String text; final void Function()? onPressed; + final ButtonStyle? style; const PlatformButton( - {super.key, required this.text, required this.onPressed}); + {super.key, required this.text, required this.onPressed, this.style}); @override - TextButton createAndroidWidget(BuildContext context) => - TextButton(onPressed: onPressed, child: Text(text)); + TextButton createAndroidWidget(BuildContext context) => TextButton( + onPressed: onPressed, + style: style, + child: Text(text), + ); @override CupertinoButton createIosWidget(BuildContext context) => 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 c91fa06..5a21330 100644 --- a/lib/util/graphs.dart +++ b/lib/util/graphs.dart @@ -1,47 +1,129 @@ +import 'package:currency_picker/currency_picker.dart'; +import 'package:dynamic_color/dynamic_color.dart'; 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'; -/// Monthly/Yearly expenses [LineChart] -class ExpensesChart extends StatelessWidget { - const ExpensesChart( +/// Monthly/Yearly expense/income [LineChart] +class ExpensesLineChart extends StatelessWidget { + const ExpensesLineChart( {super.key, required this.date, required this.locale, - this.data = const [], + required this.expenseData, + required this.incomeData, + required this.currency, this.yearly = false}); final bool yearly; final DateTime date; final String locale; - final List data; - List get dataSorted { - var list = List.from(data); + final List expenseData; + final Currency currency; + List get expenseDataSorted { + var list = List.from(expenseData); list.sort((a, b) => a.compareTo(b)); return list; } + 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) { return LineChart( LineChartData( - maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(), - maxY: dataSorted.last, - minX: 1, - minY: 0, - backgroundColor: Theme.of(context).colorScheme.background, - lineBarsData: [ - LineChartBarData( - isCurved: true, - barWidth: 8, - isStrokeCapRound: true, - dotData: const FlDotData(show: false), - belowBarData: BarAreaData(show: false), - color: Theme.of(context).colorScheme.primary, - spots: List.generate( - (yearly) ? 12 : DateTime(date.year, date.month, 0).day, - (index) => FlSpot(index.toDouble() + 1, data[index]), + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipItems: (spots) => List.generate( + spots.length, + (index) => LineTooltipItem( + (spots[index].barIndex == 0) + ? (yearly + ? AppLocalizations.of(context).incomeForMonth( + 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).incomeForDay( + NumberFormat.compactCurrency( + locale: locale, + symbol: currency.symbol, + name: currency.name) + .format(spots[index].y), + )) + : (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, + 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) + LineChartBarData( + isCurved: true, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + color: Colors.green + .harmonizeWith(Theme.of(context).colorScheme.secondary), + spots: List.generate( + (yearly) ? 12 : DateTime(date.year, date.month, 0).day, + (index) => FlSpot(index.toDouble(), incomeData[index]), + ), + ), + if (expenseData.isNotEmpty) + LineChartBarData( + isCurved: true, + barWidth: 8, + isStrokeCapRound: true, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + color: Colors.red + .harmonizeWith(Theme.of(context).colorScheme.secondary), + spots: List.generate( + (yearly) ? 12 : DateTime(date.year, date.month, 0).day, + (index) => FlSpot(index.toDouble() + 1, expenseData[index]), + ), + ), ], // actual data titlesData: FlTitlesData( rightTitles: const AxisTitles( @@ -52,15 +134,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)); @@ -72,3 +155,143 @@ 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: Colors.green + .harmonizeWith(Theme.of(context).colorScheme.secondary), + ), + if (expenseData.isNotEmpty) + BarChartRodData( + toY: expenseData[index], + color: Colors.red + .harmonizeWith(Theme.of(context).colorScheme.secondary), + ), + ], + ), + ), + ), + ); +} 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 e03c7c6..188e231 100644 --- a/lib/views/graph_view.dart +++ b/lib/views/graph_view.dart @@ -1,6 +1,6 @@ -import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:prasule/api/category.dart'; import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/main.dart'; @@ -11,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}); @@ -25,6 +26,7 @@ class _GraphViewState extends State { List wallets = []; String? locale; var yearlyBtnSet = {"monthly"}; + var graphTypeSet = {"expense", "income"}; bool get yearly => yearlyBtnSet.contains("yearly"); @override @@ -33,20 +35,22 @@ class _GraphViewState extends State { locale ??= Localizations.localeOf(context).languageCode; } - List generateChartData() { - if (selectedWallet == null) return [0]; + List generateChartData(EntryType type) { var data = List.filled( (yearly) ? 12 : DateTime(_selectedDate.year, _selectedDate.month, 0).day, 0.0); + if (selectedWallet == null) return []; for (var i = 0; i < data.length; i++) { - var entriesForRange = selectedWallet!.entries.where((element) => (!yearly) - ? element.date.month == _selectedDate.month && - element.date.year == _selectedDate.year && - element.date.day == i + 1 - : element.date.month == i + 1 && - element.date.year == _selectedDate.year); + var entriesForRange = selectedWallet!.entries.where((element) => + ((!yearly) + ? element.date.month == _selectedDate.month && + element.date.year == _selectedDate.year && + element.date.day == i + 1 + : element.date.month == i + 1 && + element.date.year == _selectedDate.year) && + element.type == type); var sum = 0.0; for (var e in entriesForRange) { sum += e.data.amount; @@ -67,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 @@ -91,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 { @@ -116,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"); } }, @@ -139,88 +148,160 @@ 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: "spending", - label: Text(AppLocalizations.of(context)!.spendingStats), - ) - ], - selected: const {"spending"}, - // TODO: onSelectionChanged - ), - 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; - 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, - data: generateChartData(), - ), + child: (selectedWallet == null) + ? const CircularProgressIndicator( + strokeWidth: 5, ) - ], - ), - ), + : SizedBox( + width: MediaQuery.of(context).size.width, + 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( + 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 { + 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 6d6e5ef..23b8dc7 100644 --- a/lib/views/home.dart +++ b/lib/views/home.dart @@ -1,6 +1,4 @@ import 'dart:math'; - -import 'package:currency_picker/currency_picker.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -74,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; @@ -87,7 +85,9 @@ class _HomeViewState extends State { name: "Test Entry #${i + 1}", amount: random.nextInt(20000).toDouble(), ), - type: EntryType.expense, + type: (random.nextInt(3) > 0) + ? EntryType.expense + : EntryType.income, date: DateTime( 2023, random.nextInt(12) + 1, @@ -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..3ab16b7 100644 --- a/lib/views/settings/settings.dart +++ b/lib/views/settings/settings.dart @@ -1,7 +1,13 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:prasule/main.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'; +import 'package:shared_preferences/shared_preferences.dart'; class SettingsView extends StatefulWidget { const SettingsView({super.key}); @@ -11,10 +17,21 @@ class SettingsView extends StatefulWidget { } class _SettingsViewState extends State { + var _useMaterialYou = true; + final _supportsYou = MyApp.appliedYou; + @override + void initState() { + super.initState(); + SharedPreferences.getInstance().then((s) { + _useMaterialYou = s.getBool("useMaterialYou") ?? true; + setState(() {}); + }); + } + @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 +41,43 @@ 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(), + ), + ), + ), + if (Platform.isAndroid && _supportsYou) + SettingsTile.switchTile( + initialValue: _useMaterialYou, + onToggle: (v) async { + var s = await SharedPreferences.getInstance(); + s.setBool("useMaterialYou", v); + _useMaterialYou = v; + setState(() {}); + }, + title: Text(AppLocalizations.of(context).enableYou), + description: Text(AppLocalizations.of(context).enableYouDesc), + ) + ], + ) ], ), ); 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 e4109b8..d8bb9d3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: build_resolvers - sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" + sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 url: "https://pub.dev" source: hosted - version: "8.8.0" + version: "8.8.1" characters: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: code_builder - sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f + sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.9.0" collection: dependency: transitive description: @@ -173,10 +173,10 @@ packages: dependency: transitive description: name: coverage - sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.7.1" + version: "1.7.2" cross_file: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: currency_picker - sha256: "5b87c259dbdb4e032c6b9abd22158782868505b5217b453c6c36445612a3d34c" + sha256: eb75deb7bc92e3f31e1b8ad4efacf71371e8e49d7a0eebd1c1a8e9fae58cc23d url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.20" dart_style: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" + sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b url: "https://pub.dev" source: hosted - version: "1.6.8" + version: "1.6.9" equatable: dependency: transitive description: @@ -531,18 +531,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" + sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f + sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe url: "https://pub.dev" source: hosted - version: "0.8.8+2" + version: "0.8.9+1" image_picker_for_web: dependency: transitive description: @@ -555,10 +555,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" + sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf url: "https://pub.dev" source: hosted - version: "0.8.8+4" + version: "0.8.9" image_picker_linux: dependency: transitive description: @@ -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: @@ -917,10 +973,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1077,10 +1133,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "045ec2137c27bf1a32e6ffa0e734d532a6677bf9016a0d1a406c54e499ff945b" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" webdriver: dependency: transitive description: @@ -1131,4 +1187,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.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: