feat: add graphs #16

Merged
hernik merged 9 commits from dev into main 2023-12-25 21:43:25 +01:00
21 changed files with 949 additions and 269 deletions
Showing only changes of commit 7f527f3d44 - Show all commits

@ -1 +1 @@
Subproject commit 593a031efb1e54aef4d083fc810482c2877ba1d2 Subproject commit fed06b31d938f7620ea7417295b8d8d19cf7cf1d

View file

@ -2,6 +2,7 @@
"conventionalCommits.scopes": [ "conventionalCommits.scopes": [
"ocr", "ocr",
"ui", "ui",
"translations" "translations",
"graphs"
] ]
} }

View file

@ -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 # 1.0.0-alpha
- First public release - First public release

View file

@ -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);
}
}

View file

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View file

@ -1,3 +1,4 @@
arb-dir: lib/l10n arb-dir: lib/l10n
template-arb-file: app_en.arb template-arb-file: app_en.arb
output-localization-file: app_localizations.dart output-localization-file: app_localizations.dart
nullable-getter: false

View file

@ -1,4 +1,5 @@
{ {
"@@locale": "cs",
"categoryHealth": "Zdraví", "categoryHealth": "Zdraví",
"categoryCar": "Auto", "categoryCar": "Auto",
"categoryFood": "Jídlo", "categoryFood": "Jídlo",
@ -48,9 +49,9 @@
"ocr": "OCR", "ocr": "OCR",
"ocrData": "OCR Data", "ocrData": "OCR Data",
"downloaded": "Staženo", "downloaded": "Staženo",
"deleteOcr": "Opravdu chcete smazat '$lang' OCR data?\nJiž tato data nebudete moct použít při skenování obrázků.", "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...", "langDownloadDialog": "Stahuji {lang}, vyčkejte prosím...",
"langDownloadProgress": "Postup: $progress %", "langDownloadProgress": "Postup: {progress} %",
"addingFromOcr": "Přidat skrz OCR", "addingFromOcr": "Přidat skrz OCR",
"license":"©️ 2023 Matyáš Caras\nVydáno pod licencí GNU AGPL license verze 3", "license":"©️ 2023 Matyáš Caras\nVydáno pod licencí GNU AGPL license verze 3",
"description":"Popis", "description":"Popis",
@ -59,8 +60,20 @@
"setupStartingBalance":"Počáteční zůstatek", "setupStartingBalance":"Počáteční zůstatek",
"graphs":"Grafy", "graphs":"Grafy",
"createTestData":"Vytvořit vzorková data", "createTestData":"Vytvořit vzorková data",
"spendingStats":"Statistiky utrácení",
"yearly":"Roční", "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"
} }

View file

@ -1,4 +1,5 @@
{ {
"@@locale": "en",
"categoryHealth": "Health", "categoryHealth": "Health",
"categoryCar": "Car", "categoryCar": "Car",
"categoryFood": "Food", "categoryFood": "Food",
@ -57,7 +58,7 @@
"ocr": "OCR", "ocr": "OCR",
"ocrData": "OCR Data", "ocrData": "OCR Data",
"downloaded": "Downloaded", "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": { "@deleteOcr": {
"description": "Shown when a user wants to delete OCR data through settings", "description": "Shown when a user wants to delete OCR data through settings",
"placeholders": { "placeholders": {
@ -67,7 +68,7 @@
} }
} }
}, },
"langDownloadDialog": "Downloading $lang, please wait...", "langDownloadDialog": "Downloading {lang}, please wait...",
"@langDownloadDialog": { "@langDownloadDialog": {
"description": "Shown as a title of a dialog while downloading new OCR data", "description": "Shown as a title of a dialog while downloading new OCR data",
"placeholders": { "placeholders": {
@ -77,7 +78,7 @@
} }
} }
}, },
"langDownloadProgress": "Download progress: $progress %", "langDownloadProgress": "Download progress: {progress} %",
"@langDownloadProgress": { "@langDownloadProgress": {
"description": "Progress percentage shown while downloading OCR data", "description": "Progress percentage shown while downloading OCR data",
"placeholders": { "placeholders": {
@ -95,7 +96,63 @@
"setupStartingBalance": "Starting balance", "setupStartingBalance": "Starting balance",
"graphs": "Graphs", "graphs": "Graphs",
"createTestData": "Create test data", "createTestData": "Create test data",
"spendingStats":"Spending statistics",
"yearly": "Yearly", "yearly": "Yearly",
"monthly":"Monthly" "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"
} }

View file

@ -8,8 +8,13 @@ import 'package:logger/logger.dart';
import 'package:prasule/util/color_schemes.g.dart'; import 'package:prasule/util/color_schemes.g.dart';
import 'package:prasule/views/home.dart'; import 'package:prasule/views/home.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.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()); runApp(const MyApp());
} }
@ -17,13 +22,15 @@ final logger = Logger();
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
static bool appliedYou = false;
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return (Platform.isAndroid) return (Platform.isAndroid)
? DynamicColorBuilder( ? DynamicColorBuilder(
builder: (light, dark) => MaterialApp( builder: (light, dark) {
appliedYou = light != null;
return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
@ -33,13 +40,19 @@ class MyApp extends StatelessWidget {
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
title: 'Prašule', title: 'Prašule',
theme: ThemeData( theme: ThemeData(
colorScheme: light ?? lightColorScheme, colorScheme: (_materialYou)
? light ?? lightColorScheme
: lightColorScheme,
useMaterial3: true, useMaterial3: true,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, colorScheme: dark ?? darkColorScheme), useMaterial3: true,
colorScheme: (_materialYou)
? dark ?? darkColorScheme
: darkColorScheme),
home: const HomeView(), home: const HomeView(),
), );
},
) )
: Theme( : Theme(
data: ThemeData( data: ThemeData(

View file

@ -5,12 +5,16 @@ import 'package:prasule/pw/platformwidget.dart';
class PlatformButton extends PlatformWidget<TextButton, CupertinoButton> { class PlatformButton extends PlatformWidget<TextButton, CupertinoButton> {
final String text; final String text;
final void Function()? onPressed; final void Function()? onPressed;
final ButtonStyle? style;
const PlatformButton( const PlatformButton(
{super.key, required this.text, required this.onPressed}); {super.key, required this.text, required this.onPressed, this.style});
@override @override
TextButton createAndroidWidget(BuildContext context) => TextButton createAndroidWidget(BuildContext context) => TextButton(
TextButton(onPressed: onPressed, child: Text(text)); onPressed: onPressed,
style: style,
child: Text(text),
);
@override @override
CupertinoButton createIosWidget(BuildContext context) => CupertinoButton createIosWidget(BuildContext context) =>

View file

@ -12,7 +12,7 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
ListTile( ListTile(
leading: const Icon(Icons.home), leading: const Icon(Icons.home),
title: Text( title: Text(
AppLocalizations.of(context)!.home, AppLocalizations.of(context).home,
), ),
selected: page == 1, selected: page == 1,
onTap: () { onTap: () {
@ -27,7 +27,7 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
ListTile( ListTile(
leading: const Icon(Icons.bar_chart), leading: const Icon(Icons.bar_chart),
title: Text( title: Text(
AppLocalizations.of(context)!.graphs, AppLocalizations.of(context).graphs,
), ),
selected: page == 2, selected: page == 2,
onTap: () { onTap: () {

View file

@ -1,45 +1,127 @@
import 'package:currency_picker/currency_picker.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Monthly/Yearly expenses [LineChart] /// Monthly/Yearly expense/income [LineChart]
class ExpensesChart extends StatelessWidget { class ExpensesLineChart extends StatelessWidget {
const ExpensesChart( const ExpensesLineChart(
{super.key, {super.key,
required this.date, required this.date,
required this.locale, required this.locale,
this.data = const [], required this.expenseData,
required this.incomeData,
required this.currency,
this.yearly = false}); this.yearly = false});
final bool yearly; final bool yearly;
final DateTime date; final DateTime date;
final String locale; final String locale;
final List<double> data; final List<double> expenseData;
List<double> get dataSorted { final Currency currency;
var list = List<double>.from(data); List<double> get expenseDataSorted {
var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b)); list.sort((a, b) => a.compareTo(b));
return list; return list;
} }
final List<double> incomeData;
List<double> get incomeDataSorted {
var list = List<double>.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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LineChart( return LineChart(
LineChartData( LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (spots) => List<LineTooltipItem>.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(), maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
maxY: dataSorted.last,
minX: 1,
minY: 0, minY: 0,
minX: 0,
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
lineBarsData: [ lineBarsData: [
if (incomeData.isNotEmpty)
LineChartBarData( LineChartBarData(
isCurved: true, isCurved: true,
barWidth: 8, barWidth: 8,
isStrokeCapRound: true, isStrokeCapRound: true,
dotData: const FlDotData(show: false), dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false), belowBarData: BarAreaData(show: false),
color: Theme.of(context).colorScheme.primary, color: Colors.green
.harmonizeWith(Theme.of(context).colorScheme.secondary),
spots: List.generate( spots: List.generate(
(yearly) ? 12 : DateTime(date.year, date.month, 0).day, (yearly) ? 12 : DateTime(date.year, date.month, 0).day,
(index) => FlSpot(index.toDouble() + 1, data[index]), (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 ], // actual data
@ -52,15 +134,16 @@ class ExpensesChart extends StatelessWidget {
), ),
bottomTitles: AxisTitles( bottomTitles: AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
reservedSize: 30,
showTitles: true, showTitles: true,
getTitlesWidget: (value, meta) { getTitlesWidget: (value, meta) {
String text; String text;
if (yearly) { if (yearly) {
text = DateFormat.MMM(locale).format( text = DateFormat.MMM(locale).format(
DateTime(date.year, value.toInt(), 1), DateTime(date.year, value.toInt() + 1, 1),
); );
} else { } else {
text = value.toInt().toString(); text = (value.toInt() + 1).toString();
} }
return SideTitleWidget( return SideTitleWidget(
axisSide: meta.axisSide, child: Text(text)); 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<double> expenseData;
List<double> get expenseDataSorted {
var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b));
return list;
}
final Currency currency;
final List<double> incomeData;
List<double> get incomeDataSorted {
var list = List<double>.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<BarChartGroupData>.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),
),
],
),
),
),
);
}

View file

@ -40,7 +40,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.createEntry), title: Text(AppLocalizations.of(context).createEntry),
), ),
body: SizedBox( body: SizedBox(
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@ -53,7 +53,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * 0.8, width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField( child: PlatformField(
labelText: AppLocalizations.of(context)!.name, labelText: AppLocalizations.of(context).name,
controller: TextEditingController(text: newEntry.data.name), controller: TextEditingController(text: newEntry.data.name),
onChanged: (v) { onChanged: (v) {
newEntry.data.name = v; newEntry.data.name = v;
@ -66,7 +66,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * 0.8, width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField( child: PlatformField(
labelText: AppLocalizations.of(context)!.amount, labelText: AppLocalizations.of(context).amount,
controller: TextEditingController( controller: TextEditingController(
text: newEntry.data.amount.toString()), text: newEntry.data.amount.toString()),
keyboardType: keyboardType:
@ -83,7 +83,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
Text(AppLocalizations.of(context)!.type), Text(AppLocalizations.of(context).type),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
@ -97,7 +97,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 24, width: MediaQuery.of(context).size.width * 0.8 - 24,
child: Text( child: Text(
AppLocalizations.of(context)!.expense, AppLocalizations.of(context).expense,
), ),
), ),
), ),
@ -105,7 +105,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
value: EntryType.income, value: EntryType.income,
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 24, 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<CreateEntryView> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
Text(AppLocalizations.of(context)!.category), Text(AppLocalizations.of(context).category),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
@ -151,7 +151,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
const SizedBox( const SizedBox(
height: 20, height: 20,
), ),
Text(AppLocalizations.of(context)!.description), Text(AppLocalizations.of(context).description),
const SizedBox( const SizedBox(
height: 10, height: 10,
), ),
@ -175,14 +175,14 @@ class _CreateEntryViewState extends State<CreateEntryView> {
height: 15, height: 15,
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.save, text: AppLocalizations.of(context).save,
onPressed: () { onPressed: () {
if (newEntry.data.name.isEmpty) { if (newEntry.data.name.isEmpty) {
ScaffoldMessenger.of(context).clearSnackBars(); ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Text(
AppLocalizations.of(context)!.errorEmptyName), AppLocalizations.of(context).errorEmptyName),
), ),
); );
return; return;

View file

@ -1,6 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/walletmanager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/main.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:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/settings/settings.dart';
import 'package:prasule/views/setup.dart'; import 'package:prasule/views/setup.dart';
import 'package:shared_preferences/shared_preferences.dart';
class GraphView extends StatefulWidget { class GraphView extends StatefulWidget {
const GraphView({super.key}); const GraphView({super.key});
@ -25,6 +26,7 @@ class _GraphViewState extends State<GraphView> {
List<Wallet> wallets = []; List<Wallet> wallets = [];
String? locale; String? locale;
var yearlyBtnSet = {"monthly"}; var yearlyBtnSet = {"monthly"};
var graphTypeSet = {"expense", "income"};
bool get yearly => yearlyBtnSet.contains("yearly"); bool get yearly => yearlyBtnSet.contains("yearly");
@override @override
@ -33,20 +35,22 @@ class _GraphViewState extends State<GraphView> {
locale ??= Localizations.localeOf(context).languageCode; locale ??= Localizations.localeOf(context).languageCode;
} }
List<double> generateChartData() { List<double> generateChartData(EntryType type) {
if (selectedWallet == null) return [0];
var data = List<double>.filled( var data = List<double>.filled(
(yearly) (yearly)
? 12 ? 12
: DateTime(_selectedDate.year, _selectedDate.month, 0).day, : DateTime(_selectedDate.year, _selectedDate.month, 0).day,
0.0); 0.0);
if (selectedWallet == null) return [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var entriesForRange = selectedWallet!.entries.where((element) => (!yearly) var entriesForRange = selectedWallet!.entries.where((element) =>
((!yearly)
? element.date.month == _selectedDate.month && ? element.date.month == _selectedDate.month &&
element.date.year == _selectedDate.year && element.date.year == _selectedDate.year &&
element.date.day == i + 1 element.date.day == i + 1
: element.date.month == i + 1 && : element.date.month == i + 1 &&
element.date.year == _selectedDate.year); element.date.year == _selectedDate.year) &&
element.type == type);
var sum = 0.0; var sum = 0.0;
for (var e in entriesForRange) { for (var e in entriesForRange) {
sum += e.data.amount; sum += e.data.amount;
@ -67,10 +71,15 @@ class _GraphViewState extends State<GraphView> {
setState(() {}); setState(() {});
} }
int? chartType;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadWallet(); loadWallet();
SharedPreferences.getInstance().then((s) {
chartType = s.getInt("monthlygraph") ?? 2;
setState(() {});
});
} }
@override @override
@ -91,7 +100,7 @@ class _GraphViewState extends State<GraphView> {
), ),
DropdownMenuItem( DropdownMenuItem(
value: -1, value: -1,
child: Text(AppLocalizations.of(context)!.newWallet), child: Text(AppLocalizations.of(context).newWallet),
) )
], ],
onChanged: (v) async { onChanged: (v) async {
@ -116,20 +125,20 @@ class _GraphViewState extends State<GraphView> {
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) => [ itemBuilder: (context) => [
AppLocalizations.of(context)!.settings, AppLocalizations.of(context).settings,
AppLocalizations.of(context)!.about AppLocalizations.of(context).about
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
onSelected: (value) { onSelected: (value) {
if (value == AppLocalizations.of(context)!.settings) { if (value == AppLocalizations.of(context).settings) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const SettingsView(), builder: (context) => const SettingsView(),
), ),
); );
} else if (value == AppLocalizations.of(context)!.about) { } else if (value == AppLocalizations.of(context).about) {
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationLegalese: AppLocalizations.of(context)!.license, applicationLegalese: AppLocalizations.of(context).license,
applicationName: "Prašule"); applicationName: "Prašule");
} }
}, },
@ -139,8 +148,12 @@ class _GraphViewState extends State<GraphView> {
drawer: makeDrawer(context, 2), drawer: makeDrawer(context, 2),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
child: SizedBox( child: (selectedWallet == null)
width: MediaQuery.of(context).size.width * 0.9, ? const CircularProgressIndicator(
strokeWidth: 5,
)
: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.height,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -148,12 +161,20 @@ class _GraphViewState extends State<GraphView> {
SegmentedButton<String>( SegmentedButton<String>(
segments: [ segments: [
ButtonSegment<String>( ButtonSegment<String>(
value: "spending", value: "expense",
label: Text(AppLocalizations.of(context)!.spendingStats), label: Text(AppLocalizations.of(context).expenses),
) ),
ButtonSegment<String>(
value: "income",
label: Text(AppLocalizations.of(context).income),
),
], ],
selected: const {"spending"}, selected: graphTypeSet,
// TODO: onSelectionChanged multiSelectionEnabled: true,
onSelectionChanged: (selection) {
graphTypeSet = selection;
setState(() {});
},
), ),
const SizedBox( const SizedBox(
height: 5, height: 5,
@ -162,36 +183,63 @@ class _GraphViewState extends State<GraphView> {
segments: [ segments: [
ButtonSegment<String>( ButtonSegment<String>(
value: "yearly", value: "yearly",
label: Text(AppLocalizations.of(context)!.yearly), label: Text(AppLocalizations.of(context).yearly),
), ),
ButtonSegment<String>( ButtonSegment<String>(
value: "monthly", value: "monthly",
label: Text(AppLocalizations.of(context)!.monthly), label: Text(AppLocalizations.of(context).monthly),
), ),
], ],
selected: yearlyBtnSet, selected: yearlyBtnSet,
onSelectionChanged: (selection) { onSelectionChanged: (selection) async {
yearlyBtnSet = selection; yearlyBtnSet = selection;
var s = await SharedPreferences.getInstance();
chartType = (yearly)
? (s.getInt("yearlygraph") ?? 1)
: (s.getInt("monthlygraph") ?? 2);
setState(() {}); setState(() {});
}, },
), ),
const SizedBox(height: 5), 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( PlatformButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.primary),
foregroundColor: MaterialStateProperty.all(
Theme.of(context)
.colorScheme
.onPrimary)),
text: (yearly) text: (yearly)
? DateFormat.y(locale).format(_selectedDate) ? DateFormat.y(locale).format(_selectedDate)
: DateFormat.yMMMM(locale).format(_selectedDate), : DateFormat.yMMMM(locale)
.format(_selectedDate),
onPressed: () async { onPressed: () async {
var firstDate = (selectedWallet!.entries var firstDate = (selectedWallet!.entries
..sort((a, b) => a.date.compareTo(b.date))) ..sort(
(a, b) => a.date.compareTo(b.date)))
.first .first
.date; .date;
var lastDate = (selectedWallet!.entries var lastDate = (selectedWallet!.entries
..sort((a, b) => b.date.compareTo(a.date))) ..sort(
(a, b) => b.date.compareTo(a.date)))
.first .first
.date; .date;
logger.i(firstDate);
logger.i(lastDate);
var newDate = await showDatePicker( var newDate = await showDatePicker(
context: context, context: context,
initialDate: _selectedDate, initialDate: DateTime(_selectedDate.year,
_selectedDate.month, 1),
firstDate: firstDate, firstDate: firstDate,
lastDate: lastDate, lastDate: lastDate,
initialEntryMode: (yearly) initialEntryMode: (yearly)
@ -211,11 +259,44 @@ class _GraphViewState extends State<GraphView> {
SizedBox( SizedBox(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
height: 300, height: 300,
child: ExpensesChart( child: (chartType == null)
? const CircularProgressIndicator()
: (chartType == 1)
? ExpensesBarChart(
currency: selectedWallet!.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
data: generateChartData(), 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)
: [],
),
)
],
),
), ),
) )
], ],

View file

@ -1,6 +1,4 @@
import 'dart:math'; import 'dart:math';
import 'package:currency_picker/currency_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
@ -74,7 +72,7 @@ class _HomeViewState extends State<HomeView> {
if (kDebugMode) if (kDebugMode)
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.bug_report), child: const Icon(Icons.bug_report),
label: AppLocalizations.of(context)!.createTestData, label: AppLocalizations.of(context).createTestData,
onTap: () { onTap: () {
// debug option to quickly fill a wallet with data // debug option to quickly fill a wallet with data
if (selectedWallet == null) return; if (selectedWallet == null) return;
@ -87,7 +85,9 @@ class _HomeViewState extends State<HomeView> {
name: "Test Entry #${i + 1}", name: "Test Entry #${i + 1}",
amount: random.nextInt(20000).toDouble(), amount: random.nextInt(20000).toDouble(),
), ),
type: EntryType.expense, type: (random.nextInt(3) > 0)
? EntryType.expense
: EntryType.income,
date: DateTime( date: DateTime(
2023, 2023,
random.nextInt(12) + 1, random.nextInt(12) + 1,
@ -114,7 +114,7 @@ class _HomeViewState extends State<HomeView> {
), ),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.edit), child: const Icon(Icons.edit),
label: AppLocalizations.of(context)!.addNew, label: AppLocalizations.of(context).addNew,
onTap: () async { onTap: () async {
var sw = await Navigator.of(context).push<Wallet>( var sw = await Navigator.of(context).push<Wallet>(
MaterialPageRoute( MaterialPageRoute(
@ -128,7 +128,7 @@ class _HomeViewState extends State<HomeView> {
}), }),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.camera_alt), child: const Icon(Icons.camera_alt),
label: AppLocalizations.of(context)!.addCamera, label: AppLocalizations.of(context).addCamera,
onTap: () async { onTap: () async {
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
final XFile? media = final XFile? media =
@ -138,7 +138,7 @@ class _HomeViewState extends State<HomeView> {
), ),
SpeedDialChild( SpeedDialChild(
child: const Icon(Icons.image), child: const Icon(Icons.image),
label: AppLocalizations.of(context)!.addGallery, label: AppLocalizations.of(context).addGallery,
onTap: () { onTap: () {
startOcr(ImageSource.gallery); startOcr(ImageSource.gallery);
}, },
@ -160,7 +160,7 @@ class _HomeViewState extends State<HomeView> {
), ),
DropdownMenuItem( DropdownMenuItem(
value: -1, value: -1,
child: Text(AppLocalizations.of(context)!.newWallet), child: Text(AppLocalizations.of(context).newWallet),
) )
], ],
onChanged: (v) async { onChanged: (v) async {
@ -185,20 +185,20 @@ class _HomeViewState extends State<HomeView> {
actions: [ actions: [
PopupMenuButton( PopupMenuButton(
itemBuilder: (context) => [ itemBuilder: (context) => [
AppLocalizations.of(context)!.settings, AppLocalizations.of(context).settings,
AppLocalizations.of(context)!.about AppLocalizations.of(context).about
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(), ].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
onSelected: (value) { onSelected: (value) {
if (value == AppLocalizations.of(context)!.settings) { if (value == AppLocalizations.of(context).settings) {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const SettingsView(), builder: (context) => const SettingsView(),
), ),
); );
} else if (value == AppLocalizations.of(context)!.about) { } else if (value == AppLocalizations.of(context).about) {
showAboutDialog( showAboutDialog(
context: context, context: context,
applicationLegalese: AppLocalizations.of(context)!.license, applicationLegalese: AppLocalizations.of(context).license,
applicationName: "Prašule"); applicationName: "Prašule");
} }
}, },
@ -223,14 +223,14 @@ class _HomeViewState extends State<HomeView> {
? Column( ? Column(
children: [ children: [
Text( Text(
AppLocalizations.of(context)!.noEntries, AppLocalizations.of(context).noEntries,
style: const TextStyle( style: const TextStyle(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text( Text(
AppLocalizations.of(context)!.noEntriesSub, AppLocalizations.of(context).noEntriesSub,
) )
], ],
) )
@ -307,12 +307,12 @@ class _HomeViewState extends State<HomeView> {
context: context, context: context,
builder: (cx) => PlatformDialog( builder: (cx) => PlatformDialog(
title: title:
AppLocalizations.of(context)!.sureDialog, AppLocalizations.of(context).sureDialog,
content: Text( content: Text(
AppLocalizations.of(context)!.deleteSure), AppLocalizations.of(context).deleteSure),
actions: [ actions: [
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.yes, text: AppLocalizations.of(context).yes,
onPressed: () { onPressed: () {
selectedWallet?.entries.removeWhere( selectedWallet?.entries.removeWhere(
(e) => e.id == element.id); (e) => e.id == element.id);
@ -323,7 +323,7 @@ class _HomeViewState extends State<HomeView> {
}, },
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.no, text: AppLocalizations.of(context).no,
onPressed: () { onPressed: () {
Navigator.of(cx).pop(); Navigator.of(cx).pop();
}, },
@ -365,9 +365,9 @@ class _HomeViewState extends State<HomeView> {
if (!mounted) return; if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context)!.missingOcr), content: Text(AppLocalizations.of(context).missingOcr),
action: SnackBarAction( action: SnackBarAction(
label: AppLocalizations.of(context)!.download, label: AppLocalizations.of(context).download,
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
@ -408,7 +408,7 @@ class _HomeViewState extends State<HomeView> {
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
title: AppLocalizations.of(context)!.ocrLoading), title: AppLocalizations.of(context).ocrLoading),
barrierDismissible: false); barrierDismissible: false);
var string = await FlutterTesseractOcr.extractText(media.path, var string = await FlutterTesseractOcr.extractText(media.path,
language: selected, language: selected,
@ -472,7 +472,7 @@ class _HomeViewState extends State<HomeView> {
child: const Text("Cancel"), child: const Text("Cancel"),
), ),
], ],
title: AppLocalizations.of(context)!.ocrSelect, title: AppLocalizations.of(context).ocrSelect,
content: Column( content: Column(
children: [ children: [
...List.generate( ...List.generate(

View file

@ -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<GraphTypeSettingsView> createState() => _GraphTypeSettingsViewState();
}
class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
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(() {});
},
),
),
],
),
),
),
),
],
)
],
),
);
}
}

View file

@ -1,7 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart'; 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:prasule/views/settings/tessdata_list.dart';
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
const SettingsView({super.key}); const SettingsView({super.key});
@ -11,10 +17,21 @@ class SettingsView extends StatefulWidget {
} }
class _SettingsViewState extends State<SettingsView> { class _SettingsViewState extends State<SettingsView> {
var _useMaterialYou = true;
final _supportsYou = MyApp.appliedYou;
@override
void initState() {
super.initState();
SharedPreferences.getInstance().then((s) {
_useMaterialYou = s.getBool("useMaterialYou") ?? true;
setState(() {});
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings)), appBar: AppBar(title: Text(AppLocalizations.of(context).settings)),
body: SettingsList( body: SettingsList(
applicationType: ApplicationType.both, applicationType: ApplicationType.both,
darkTheme: SettingsThemeData( darkTheme: SettingsThemeData(
@ -24,16 +41,43 @@ class _SettingsViewState extends State<SettingsView> {
SettingsSection( SettingsSection(
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
title: Text(AppLocalizations.of(context)!.downloadedOcr), title: Text(AppLocalizations.of(context).downloadedOcr),
description: description:
Text(AppLocalizations.of(context)!.downloadedOcrDesc), Text(AppLocalizations.of(context).downloadedOcrDesc),
onPressed: (context) => Navigator.of(context).push( trailing: const Icon(Icons.keyboard_arrow_right),
MaterialPageRoute( onPressed: (context) => Navigator.of(context)
builder: (c) => const TessdataListView())), .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),
)
],
)
], ],
), ),
); );

View file

@ -29,7 +29,7 @@ class _TessdataListViewState extends State<TessdataListView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.ocrData)), appBar: AppBar(title: Text(AppLocalizations.of(context).ocrData)),
body: Center( body: Center(
child: SizedBox( child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
@ -50,8 +50,8 @@ class _TessdataListViewState extends State<TessdataListView> {
title: Text(_tessdata[i].keys.first), title: Text(_tessdata[i].keys.first),
trailing: TextButton( trailing: TextButton(
child: Text(_tessdata[i][_tessdata[i].keys.first]! child: Text(_tessdata[i][_tessdata[i].keys.first]!
? AppLocalizations.of(context)!.downloaded ? AppLocalizations.of(context).downloaded
: AppLocalizations.of(context)!.download), : AppLocalizations.of(context).download),
onPressed: () async { onPressed: () async {
var lang = _tessdata[i].keys.first; var lang = _tessdata[i].keys.first;
if (_tessdata[i][lang]!) { if (_tessdata[i][lang]!) {
@ -59,12 +59,12 @@ class _TessdataListViewState extends State<TessdataListView> {
showDialog( showDialog(
context: context, context: context,
builder: (context) => PlatformDialog( builder: (context) => PlatformDialog(
title: AppLocalizations.of(context)!.sureDialog, title: AppLocalizations.of(context).sureDialog,
content: Text(AppLocalizations.of(context)! content: Text(AppLocalizations.of(context)
.deleteOcr(lang)), .deleteOcr(lang)),
actions: [ actions: [
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.yes, text: AppLocalizations.of(context).yes,
onPressed: () async { onPressed: () async {
await TessdataApi.deleteData(lang); await TessdataApi.deleteData(lang);
_tessdata[i][lang] = true; _tessdata[i][lang] = true;
@ -72,7 +72,7 @@ class _TessdataListViewState extends State<TessdataListView> {
}, },
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)!.no, text: AppLocalizations.of(context).no,
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -91,7 +91,7 @@ class _TessdataListViewState extends State<TessdataListView> {
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
title: AppLocalizations.of(context)! title: AppLocalizations.of(context)
.langDownloadDialog(lang), .langDownloadDialog(lang),
content: StreamBuilder( content: StreamBuilder(
builder: (context, snapshot) { builder: (context, snapshot) {
@ -102,7 +102,7 @@ class _TessdataListViewState extends State<TessdataListView> {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Text("Error"); return const Text("Error");
} }
return Text(AppLocalizations.of(context)! return Text(AppLocalizations.of(context)
.langDownloadProgress(snapshot.data!)); .langDownloadProgress(snapshot.data!));
}, },
stream: progressStream.stream, stream: progressStream.stream,

View file

@ -45,28 +45,28 @@ class _SetupViewState extends State<SetupView> {
if (categories.isEmpty) { if (categories.isEmpty) {
categories = [ categories = [
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)!.categoryHealth, name: AppLocalizations.of(context).categoryHealth,
type: EntryType.expense, type: EntryType.expense,
id: 1, id: 1,
icon: IconData(Icons.medical_information.codePoint, icon: IconData(Icons.medical_information.codePoint,
fontFamily: 'MaterialIcons'), fontFamily: 'MaterialIcons'),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)!.categoryCar, name: AppLocalizations.of(context).categoryCar,
type: EntryType.expense, type: EntryType.expense,
id: 2, id: 2,
icon: icon:
IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)!.categoryFood, name: AppLocalizations.of(context).categoryFood,
type: EntryType.expense, type: EntryType.expense,
id: 3, id: 3,
icon: icon:
IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)!.categoryTravel, name: AppLocalizations.of(context).categoryTravel,
type: EntryType.expense, type: EntryType.expense,
id: 4, id: 4,
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
@ -90,16 +90,16 @@ class _SetupViewState extends State<SetupView> {
showNextButton: true, showNextButton: true,
showBackButton: true, showBackButton: true,
showDoneButton: true, showDoneButton: true,
next: Text(AppLocalizations.of(context)!.next), next: Text(AppLocalizations.of(context).next),
back: Text(AppLocalizations.of(context)!.back), back: Text(AppLocalizations.of(context).back),
done: Text(AppLocalizations.of(context)!.finish), done: Text(AppLocalizations.of(context).finish),
onDone: () { onDone: () {
if (name.isEmpty) { if (name.isEmpty) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.clearSnackBars(); // TODO: iOS replacement .clearSnackBars(); // TODO: iOS replacement
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: content:
Text(AppLocalizations.of(context)!.errorEmptyName))); Text(AppLocalizations.of(context).errorEmptyName)));
return; return;
} }
var wallet = Wallet( var wallet = Wallet(
@ -113,7 +113,7 @@ class _SetupViewState extends State<SetupView> {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content:
Text(AppLocalizations.of(context)!.walletExists), Text(AppLocalizations.of(context).walletExists),
), ),
); );
return; return;
@ -137,7 +137,7 @@ class _SetupViewState extends State<SetupView> {
titleWidget: Padding( titleWidget: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.welcome, AppLocalizations.of(context).welcome,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold), fontSize: 24, fontWeight: FontWeight.bold),
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -150,7 +150,7 @@ class _SetupViewState extends State<SetupView> {
if (!widget.newWallet) if (!widget.newWallet)
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context)!.welcomeAboutPrasule), AppLocalizations.of(context).welcomeAboutPrasule),
), ),
if (!widget.newWallet) if (!widget.newWallet)
const SizedBox( const SizedBox(
@ -158,7 +158,7 @@ class _SetupViewState extends State<SetupView> {
), ),
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context)!.welcomeInstruction), AppLocalizations.of(context).welcomeInstruction),
), ),
], ],
), ),
@ -169,7 +169,7 @@ class _SetupViewState extends State<SetupView> {
titleWidget: Padding( titleWidget: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.setupWalletNameCurrency, AppLocalizations.of(context).setupWalletNameCurrency,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold), fontSize: 24, fontWeight: FontWeight.bold),
@ -182,7 +182,7 @@ class _SetupViewState extends State<SetupView> {
width: MediaQuery.of(context).size.width * 0.7, width: MediaQuery.of(context).size.width * 0.7,
child: PlatformField( child: PlatformField(
labelText: labelText:
AppLocalizations.of(context)!.setupNamePlaceholder, AppLocalizations.of(context).setupNamePlaceholder,
onChanged: (t) { onChanged: (t) {
name = t; name = t;
}, },
@ -192,7 +192,7 @@ class _SetupViewState extends State<SetupView> {
height: 5, height: 5,
), ),
PlatformButton( PlatformButton(
text: AppLocalizations.of(context)! text: AppLocalizations.of(context)
.setupCurrency(_selectedCurrency.code), .setupCurrency(_selectedCurrency.code),
onPressed: () { onPressed: () {
showCurrencyPicker( showCurrencyPicker(
@ -211,7 +211,7 @@ class _SetupViewState extends State<SetupView> {
width: MediaQuery.of(context).size.width * 0.7, width: MediaQuery.of(context).size.width * 0.7,
child: PlatformField( child: PlatformField(
labelText: labelText:
AppLocalizations.of(context)!.setupStartingBalance, AppLocalizations.of(context).setupStartingBalance,
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true), decimal: true),
inputFormatters: [ inputFormatters: [
@ -233,7 +233,7 @@ class _SetupViewState extends State<SetupView> {
titleWidget: Padding( titleWidget: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text( child: Text(
AppLocalizations.of(context)!.setupCategoriesHeading, AppLocalizations.of(context).setupCategoriesHeading,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold), fontSize: 24, fontWeight: FontWeight.bold),
@ -243,7 +243,7 @@ class _SetupViewState extends State<SetupView> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
AppLocalizations.of(context)!.setupCategoriesEditHint, AppLocalizations.of(context).setupCategoriesEditHint,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox( SizedBox(
@ -296,17 +296,17 @@ class _SetupViewState extends State<SetupView> {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
AppLocalizations.of(context)!.ok), AppLocalizations.of(context).ok),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text( child: Text(
AppLocalizations.of(context)!.cancel), AppLocalizations.of(context).cancel),
), ),
], ],
title: AppLocalizations.of(context)! title: AppLocalizations.of(context)
.setupCategoriesEditingName, .setupCategoriesEditingName,
content: SizedBox( content: SizedBox(
width: 400, width: 400,
@ -336,7 +336,7 @@ class _SetupViewState extends State<SetupView> {
} }
categories.add( categories.add(
WalletCategory( WalletCategory(
name: AppLocalizations.of(context)! name: AppLocalizations.of(context)
.setupWalletNamePlaceholder, .setupWalletNamePlaceholder,
type: EntryType.expense, type: EntryType.expense,
id: id, id: id,

View file

@ -77,10 +77,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8" sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.2"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -109,10 +109,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "69acb7007eb2a31dc901512bfe0f7b767168be34cb734835d54c070bfa74c1b2" sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.8.0" version: "8.8.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -133,10 +133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cli_util name: cli_util
sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.0" version: "0.4.1"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -149,10 +149,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_builder name: code_builder
sha256: b2151ce26a06171005b379ecff6e08d34c470180ffe16b8e14b6d52be292b55f sha256: feee43a5c05e7b3199bb375a86430b8ada1b04104f2923d0e03cc01ca87b6d84
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.9.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@ -173,10 +173,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: coverage name: coverage
sha256: ac86d3abab0f165e4b8f561280ff4e066bceaac83c424dd19f1ae2c2fcd12ca9 sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.7.1" version: "1.7.2"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -205,10 +205,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: currency_picker name: currency_picker
sha256: "5b87c259dbdb4e032c6b9abd22158782868505b5217b453c6c36445612a3d34c" sha256: eb75deb7bc92e3f31e1b8ad4efacf71371e8e49d7a0eebd1c1a8e9fae58cc23d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.19" version: "2.0.20"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
@ -237,10 +237,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dynamic_color name: dynamic_color
sha256: "8b8bd1d798bd393e11eddeaa8ae95b12ff028bf7d5998fc5d003488cd5f4ce2f" sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.8" version: "1.6.9"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@ -317,10 +317,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fl_chart name: fl_chart
sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.66.0" version: "0.65.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -531,18 +531,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84" sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.5"
image_picker_android: image_picker_android:
dependency: transitive dependency: transitive
description: description:
name: image_picker_android name: image_picker_android
sha256: d6a6e78821086b0b737009b09363018309bbc6de3fd88cc5c26bc2bb44a4957f sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.8+2" version: "0.8.9+1"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
@ -555,10 +555,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_ios name: image_picker_ios
sha256: "76ec722aeea419d03aa915c2c96bf5b47214b053899088c9abb4086ceecf97a7" sha256: eac0a62104fa12feed213596df0321f57ce5a572562f72a68c4ff81e9e4caacf
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.8+4" version: "0.8.9"
image_picker_linux: image_picker_linux:
dependency: transitive dependency: transitive
description: description:
@ -876,6 +876,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.2" 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: shelf:
dependency: transitive dependency: transitive
description: description:
@ -917,10 +973,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_gen name: source_gen
sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.5.0"
source_helper: source_helper:
dependency: transitive dependency: transitive
description: description:
@ -1077,10 +1133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: web_socket_channel name: web_socket_channel
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b sha256: "045ec2137c27bf1a32e6ffa0e734d532a6677bf9016a0d1a406c54e499ff945b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.1"
webdriver: webdriver:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,7 +1,7 @@
name: prasule name: prasule
description: Open-source private expense tracker description: Open-source private expense tracker
version: 1.0.0-alpha2+2 version: 1.0.0-alpha.2+2
environment: environment:
sdk: '>=3.1.0-262.2.beta <4.0.0' sdk: '>=3.1.0-262.2.beta <4.0.0'
@ -38,7 +38,8 @@ dependencies:
flutter_slidable: ^3.0.0 flutter_slidable: ^3.0.0
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
fl_chart: ^0.66.0 fl_chart: ^0.65.0
shared_preferences: ^2.2.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: