Compare commits

..

No commits in common. "6675f916350236e083741ed25ce88ae76ac2b835" and "7ba119f9a8a1b90ddfc70a1695561c8eda6d5968" have entirely different histories.

39 changed files with 797 additions and 2758 deletions

View file

@ -1,14 +1,5 @@
# 1.0.0-alpha+3
- Add settings view for editing wallet categories
- Change code according to more aggressive linting
- Create a default "no category" category, mainly to store entries with removed categories
- Categories now have changeable colors assigned to them
- Added pie chart for expense/income data per category
- Added recurring entries
- Fixed wrong default sorting
- Differentiate expense and income on home view
# 1.0.0-alpha+2 # 1.0.0-alpha+2
- Fixed localization issues - Fixed localization issues
- Added graphs for expenses and income per month/year - Added graphs for expenses and income per month/year
# 1.0.0-alpha # 1.0.0-alpha+1
- First public release - First public release

View file

@ -7,7 +7,7 @@
# The following line activates a set of recommended lints for Flutter apps, # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices. # packages, and plugins designed to encourage good coding practices.
include: package:very_good_analysis/analysis_options.yaml include: package:flutter_lints/flutter.yaml
linter: linter:
# The lint rules applied to this project can be customized in the # The lint rules applied to this project can be customized in the
@ -23,8 +23,6 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
prefer_single_quotes: false
flutter_style_todos: false
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View file

@ -6,33 +6,24 @@ part 'category.g.dart';
/// Represents a category in a user's wallet /// Represents a category in a user's wallet
class WalletCategory { class WalletCategory {
/// Represents a category in a user's wallet final EntryType type;
WalletCategory({
required this.name,
required this.id,
required this.icon,
required this.color,
});
/// Connects generated fromJson method
factory WalletCategory.fromJson(Map<String, dynamic> json) =>
_$WalletCategoryFromJson(json);
/// User-defined name
String name; String name;
/// Unique identificator of the category
final int id; final int id;
/// Selected Icon for the category
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson) @JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
IconData icon; IconData icon;
/// The color that will be displayed with entry WalletCategory(
@JsonKey(fromJson: _colorFromJson, toJson: _colorToJson) {required this.name,
Color color; required this.type,
required this.id,
required this.icon});
/// Connects generated toJson method /// Connect the generated [_$WalletEntry] function to the `fromJson`
/// factory.
factory WalletCategory.fromJson(Map<String, dynamic> json) =>
_$WalletCategoryFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this); Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
@override @override
@ -43,18 +34,7 @@ class WalletCategory {
Map<String, dynamic> _iconDataToJson(IconData icon) => Map<String, dynamic> _iconDataToJson(IconData icon) =>
{'codepoint': icon.codePoint, 'family': icon.fontFamily}; {'codepoint': icon.codePoint, 'family': icon.fontFamily};
IconData _iconDataFromJson(Map<String, dynamic> data) => IconData _iconDataFromJson(Map<String, dynamic> data) =>
IconData(data['codepoint'] as int, fontFamily: data['family'] as String?); IconData(data['codepoint'], fontFamily: data['family']);
int _colorToJson(Color color) => color.value; enum EntryType { expense, income }
Color _colorFromJson(int input) => Color(input);
/// Type of entry, either expense or income
enum EntryType {
/// Expense
expense,
/// Income
income
}

View file

@ -9,15 +9,20 @@ part of 'category.dart';
WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) => WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) =>
WalletCategory( WalletCategory(
name: json['name'] as String, name: json['name'] as String,
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
id: json['id'] as int, id: json['id'] as int,
icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>), icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>),
color: _colorFromJson(json['color'] as int),
); );
Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) => Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) =>
<String, dynamic>{ <String, dynamic>{
'type': _$EntryTypeEnumMap[instance.type]!,
'name': instance.name, 'name': instance.name,
'id': instance.id, 'id': instance.id,
'icon': _iconDataToJson(instance.icon), 'icon': _iconDataToJson(instance.icon),
'color': _colorToJson(instance.color), };
const _$EntryTypeEnumMap = {
EntryType.expense: 'expense',
EntryType.income: 'income',
}; };

View file

@ -1,25 +1,16 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
part 'entry_data.g.dart'; part 'entry_data.g.dart';
/// Contains raw data
@JsonSerializable() @JsonSerializable()
class EntryData { class EntryData {
/// Contains raw data String name;
String description;
double amount;
EntryData({required this.name, required this.amount, this.description = ""}); EntryData({required this.name, required this.amount, this.description = ""});
/// Connects generated fromJson method
factory EntryData.fromJson(Map<String, dynamic> json) => factory EntryData.fromJson(Map<String, dynamic> json) =>
_$EntryDataFromJson(json); _$EntryDataFromJson(json);
/// Name of entry
String name;
/// Optional description, default is empty
String description;
/// Amount for entry
double amount;
/// Connects generated toJson method
Map<String, dynamic> toJson() => _$EntryDataToJson(this); Map<String, dynamic> toJson() => _$EntryDataToJson(this);
} }

View file

@ -1,52 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/wallet_entry.dart';
part 'recurring_entry.g.dart';
/// This is a [WalletSingleEntry] that is automatically recurring
@JsonSerializable()
class RecurringWalletEntry extends WalletSingleEntry {
/// This is a [WalletSingleEntry] that is automatically recurring
RecurringWalletEntry({
required super.data,
required super.type,
required super.date,
required super.category,
required super.id,
required this.lastRunDate,
required this.recurType,
this.repeatAfter = 1,
});
/// Connects generated fromJson method
factory RecurringWalletEntry.fromJson(Map<String, dynamic> json) =>
_$RecurringWalletEntryFromJson(json);
/// Connects generated toJson method
@override
Map<String, dynamic> toJson() => _$RecurringWalletEntryToJson(this);
/// Last date the recurring entry was added into the single entry list
DateTime lastRunDate;
/// After how many {recurType} should the entry recur
int repeatAfter;
/// What type of recurrence should happen
RecurType recurType;
}
/// How a [RecurringWalletEntry] should recur
@JsonEnum()
enum RecurType {
/// Will recur every {repeatAfter} months
month,
/// Will recur every {repeatAfter} years
year,
/// Will recur every {repeatAfter} days
day
}

View file

@ -1,45 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'recurring_entry.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
RecurringWalletEntry _$RecurringWalletEntryFromJson(
Map<String, dynamic> json) =>
RecurringWalletEntry(
data: EntryData.fromJson(json['data'] as Map<String, dynamic>),
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
date: DateTime.parse(json['date'] as String),
category:
WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
id: json['id'] as int,
lastRunDate: DateTime.parse(json['lastRunDate'] as String),
repeatAfter: json['repeatAfter'] as int,
recurType: $enumDecode(_$RecurTypeEnumMap, json['recurType']),
);
Map<String, dynamic> _$RecurringWalletEntryToJson(
RecurringWalletEntry instance) =>
<String, dynamic>{
'type': _$EntryTypeEnumMap[instance.type]!,
'data': instance.data,
'date': instance.date.toIso8601String(),
'category': instance.category,
'id': instance.id,
'lastRunDate': instance.lastRunDate.toIso8601String(),
'repeatAfter': instance.repeatAfter,
'recurType': _$RecurTypeEnumMap[instance.recurType]!,
};
const _$EntryTypeEnumMap = {
EntryType.expense: 'expense',
EntryType.income: 'income',
};
const _$RecurTypeEnumMap = {
RecurType.month: 'month',
RecurType.year: 'year',
RecurType.day: 'day',
};

View file

@ -1,59 +1,36 @@
import 'package:currency_picker/currency_picker.dart'; import 'package:currency_picker/currency_picker.dart';
import 'package:intl/intl.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/wallet_entry.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart';
part 'wallet.g.dart'; part 'wallet.g.dart';
Currency _currencyFromJson(Map<String, dynamic> data) => Currency _currencyFromJson(Map<String, dynamic> data) =>
Currency.from(json: data); Currency.from(json: data);
/// Represents a single wallet
///
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
@JsonSerializable() @JsonSerializable()
class Wallet { class Wallet {
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
Wallet({
required this.name,
required this.currency,
this.categories = const [],
this.entries = const [],
this.recurringEntries = const [],
this.starterBalance = 0,
});
/// Connects generated fromJson method
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
/// A list of all [RecurringWalletEntry]s
final List<RecurringWalletEntry> recurringEntries;
/// Name of the wallet
final String name; final String name;
/// A list of available categories
final List<WalletCategory> categories; final List<WalletCategory> categories;
/// List of saved entries
final List<WalletSingleEntry> entries; final List<WalletSingleEntry> entries;
/// The starting balance of the wallet
///
/// Used to calculate current balance
double starterBalance; double starterBalance;
/// Selected currency
@JsonKey(fromJson: _currencyFromJson) @JsonKey(fromJson: _currencyFromJson)
final Currency currency; final Currency currency;
/// Connects generated toJson method Wallet(
{required this.name,
required this.currency,
this.categories = const [],
this.entries = const [],
this.starterBalance = 0});
/// Connect the generated [_$WalletEntry] function to the `fromJson`
/// factory.
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
Map<String, dynamic> toJson() => _$WalletToJson(this); Map<String, dynamic> toJson() => _$WalletToJson(this);
/// Getter for the next unused unique number ID in the wallet's **entry** list /// Getter for the next unused unique number ID in the wallet's entry list
int get nextId { int get nextId {
var id = 1; var id = 1;
while (entries.where((element) => element.id == id).isNotEmpty) { while (entries.where((element) => element.id == id).isNotEmpty) {
@ -62,107 +39,13 @@ class Wallet {
return id; return id;
} }
/// Getter for the next unused unique number ID in the wallet's **category**
/// list
int get nextCategoryId {
var id = 0;
while (categories.where((element) => element.id == id).isNotEmpty) {
id++; // create unique ID
}
return id;
}
/// Handles adding recurring entries to the entry list
void recurEntries() {
final n = DateTime.now();
for (final ent in recurringEntries) {
var m = DateTime(
(ent.recurType == RecurType.year)
? ent.lastRunDate.year + ent.repeatAfter
: ent.lastRunDate.year,
(ent.recurType == RecurType.month)
? ent.lastRunDate.month + ent.repeatAfter
: ent.lastRunDate.month,
(ent.recurType == RecurType.day)
? ent.lastRunDate.day + ent.repeatAfter
: ent.lastRunDate.day,
); // create the date after which we should recur
while (n.isAfter(
m,
)) {
logger
..i("Adding recurring entry ${ent.data.name}")
..d("Current entry count: ${entries.length}");
recurringEntries[recurringEntries.indexOf(ent)].lastRunDate =
m; // update date on recurring entry
final addedEntry = WalletSingleEntry(
data: recurringEntries[recurringEntries.indexOf(ent)].data,
type: recurringEntries[recurringEntries.indexOf(ent)].type,
date: m,
category: recurringEntries[recurringEntries.indexOf(ent)].category,
id: nextId,
);
entries.add(addedEntry);
m = DateTime(
(ent.recurType == RecurType.year)
? recurringEntries[recurringEntries.indexOf(ent)]
.lastRunDate
.year +
ent.repeatAfter
: recurringEntries[recurringEntries.indexOf(ent)]
.lastRunDate
.year,
(ent.recurType == RecurType.month)
? recurringEntries[recurringEntries.indexOf(ent)]
.lastRunDate
.month +
ent.repeatAfter
: recurringEntries[recurringEntries.indexOf(ent)]
.lastRunDate
.month,
(ent.recurType == RecurType.day)
? recurringEntries[recurringEntries.indexOf(ent)]
.lastRunDate
.day +
ent.repeatAfter
: recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day,
); // add the variable again to check if we aren't missing any entries
logger.i(
"Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})");
}
WalletManager.saveWallet(this); // save wallet
}
}
/// Removes the specified category.
///
/// All [WalletSingleEntry]s will have their category reassigned
/// to the default *No category*
Future<void> removeCategory(WalletCategory category) async {
// First remove the category from existing entries
for (final entryToChange
in entries.where((element) => element.category.id == category.id)) {
entryToChange.category =
categories.where((element) => element.id == 0).first;
}
// Remove the category
categories.removeWhere((element) => element.id == category.id);
// Save
await WalletManager.saveWallet(this);
}
/// Empty wallet used for placeholders
static final Wallet empty = Wallet( static final Wallet empty = Wallet(
name: "Empty", name: "Empty",
currency: Currency.from( currency: Currency.from(
json: { json: {
"code": "USD", "code": "USD",
"name": "United States Dollar", "name": "United States Dollar",
"symbol": r"$", "symbol": "\$",
"flag": "USD", "flag": "USD",
"decimal_digits": 2, "decimal_digits": 2,
"number": 840, "number": 840,

View file

@ -18,16 +18,10 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
(e) => WalletSingleEntry.fromJson(e as Map<String, dynamic>)) (e) => WalletSingleEntry.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
const [], const [],
recurringEntries: (json['recurringEntries'] as List<dynamic>?)
?.map((e) =>
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0, starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
); );
Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{ Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
'recurringEntries': instance.recurringEntries,
'name': instance.name, 'name': instance.name,
'categories': instance.categories, 'categories': instance.categories,
'entries': instance.entries, 'entries': instance.entries,

View file

@ -1,41 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/entry_data.dart';
part 'walletentry.g.dart';
part 'wallet_entry.g.dart';
@JsonSerializable() @JsonSerializable()
/// This is an entry containing a single item /// This is an entry containing a single item
class WalletSingleEntry { class WalletSingleEntry {
/// This is an entry containing a single item EntryType type;
WalletSingleEntry({ EntryData data;
required this.data, DateTime date;
WalletCategory category;
int id;
WalletSingleEntry(
{required this.data,
required this.type, required this.type,
required this.date, required this.date,
required this.category, required this.category,
required this.id, required this.id});
});
/// Connects generated fromJson method /// Connect the generated [_$WalletEntry] function to the `fromJson`
/// factory.
factory WalletSingleEntry.fromJson(Map<String, dynamic> json) => factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
_$WalletSingleEntryFromJson(json); _$WalletSingleEntryFromJson(json);
/// Expense or income /// Connect the generated [_$WalletEntryToJson] function to the `toJson` method.
EntryType type;
/// Actual entry data
EntryData data;
/// Date of entry creation
DateTime date;
/// Selected category
WalletCategory category;
/// Unique entry ID
int id;
/// Connects generated toJson method
Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this); Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this);
} }

View file

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'wallet_entry.dart'; part of 'walletentry.dart';
// ************************************************************************** // **************************************************************************
// JsonSerializableGenerator // JsonSerializableGenerator

View file

@ -3,52 +3,42 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/main.dart';
/// Used for [Wallet]-managing operations
class WalletManager { class WalletManager {
/// Returns a list of all [Wallet]s
static Future<List<Wallet>> listWallets() async { static Future<List<Wallet>> listWallets() async {
final path = var path =
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
final wallets = <Wallet>[]; var wallets = <Wallet>[];
for (final w for (var w in path.listSync().map((e) => e.path.split("/").last).toList()) {
in path.listSync().map((e) => e.path.split("/").last).toList()) {
try { try {
wallets.add(await loadWallet(w)); wallets.add(await loadWallet(w));
} catch (e) { } catch (e) {
logger.e(e);
// TODO: do something with unreadable wallets // TODO: do something with unreadable wallets
} }
} }
logger.i(wallets.length);
return wallets; return wallets;
} }
/// Loads and returns a single [Wallet] by name
static Future<Wallet> loadWallet(String name) async { static Future<Wallet> loadWallet(String name) async {
final path = var path =
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
final wallet = File("${path.path}/$name"); var wallet = File("${path.path}/$name");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
if (!wallet.existsSync()) { if (!wallet.existsSync()) {
return Future.error("Wallet does not exist"); return Future.error("Wallet does not exist");
} }
return Wallet.fromJson( return Wallet.fromJson(jsonDecode(wallet.readAsStringSync()));
jsonDecode(wallet.readAsStringSync()) as Map<String, dynamic>,
);
} }
/// Converts [Wallet] to JSON and saves it to AppData
static Future<bool> saveWallet(Wallet w) async { static Future<bool> saveWallet(Wallet w) async {
final path = var path =
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
final wallet = File("${path.path}/${w.name}"); var wallet = File("${path.path}/${w.name}");
if (!path.existsSync()) { if (!path.existsSync()) {
path.createSync(); path.createSync();
} }
@ -57,10 +47,10 @@ class WalletManager {
return true; return true;
} }
/// Deletes the corresponding [Wallet] file
static Future<void> deleteWallet(Wallet w) async { static Future<void> deleteWallet(Wallet w) async {
final path = var path =
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets"); Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
File("${path.path}/${w.name}").deleteSync(); var wallet = File("${path.path}/${w.name}");
wallet.deleteSync();
} }
} }

View file

@ -74,18 +74,6 @@
"barChart":"Sloupcový", "barChart":"Sloupcový",
"selectType":"Zvolte typ", "selectType":"Zvolte typ",
"enableYou":"Povolit Material You (Může vyžadovat restart aplikace)", "enableYou":"Povolit Material You (Může vyžadovat restart aplikace)",
"enableYouDesc":"Aplikace použije barevné schéma z vaší tapety", "enableYouDesc":"Aplikace použije barevné schéma z vaší tapety"
"editCategories":"Upravit kategorie",
"editCategoriesDesc":"Přidat, upravit nebo odebrat kategorii z peněženky",
"wallet":"Peněženka",
"noCategory":"Žádná kategorie",
"done":"Hotovo",
"pickColor":"Zvolte barvu",
"changeDate":"Změnit ze kterého měsíce/roku brát data",
"recurringPayments":"Opakující se platby",
"monthCounter": "{count, plural, =1{měsíc} few{měsíce} many{měsíců} other{měsíců} }",
"dayCounter":"{count, plural, =1{den} few{dny} many{dnů} other{dnů} }",
"yearCounter":"{count, plural, =1{rok} few{rok} many{let} other{let} }",
"recurEvery":"{count, plural, =1{Opakovat každý} few{Opakovat každé} many{Opakovat každých} other{Opakovat každých}}",
"startingWithDate": "počínaje datem"
} }

View file

@ -154,54 +154,5 @@
"barChart":"Bar chart", "barChart":"Bar chart",
"selectType":"Select type", "selectType":"Select type",
"enableYou":"Enable Material You (May require an app restart)", "enableYou":"Enable Material You (May require an app restart)",
"enableYouDesc":"The app will use a color scheme from your wallpaper", "enableYouDesc":"The app will use a color scheme from your wallpaper"
"editCategories":"Edit categories",
"editCategoriesDesc":"Add, edit or remove categories from a wallet",
"wallet":"Wallet",
"noCategory":"No category",
"done":"Done",
"pickColor":"Pick a color",
"changeDate":"Change what month/year to pick data from",
"recurringPayments":"Recurring payments",
"recurEvery":"{count, plural, other{Recur every}}",
"@recurEvery":{
"description": "Shown when creating recurring entries, ex.: Recur every 2 months",
"placeholders": {
"count":{
"description": "Specifies how many X are being counted",
"type": "int"
}
}
},
"monthCounter":"{count, plural, =1{month} other{months} }",
"@monthCounter":{
"placeholders": {
"count":{
"description": "Specifies how many months are being counted",
"type": "int"
}
}
},
"dayCounter":"{count, plural, =1{day} other{days} }",
"@dayCounter":{
"placeholders": {
"count":{
"description": "Specifies how many days are being counted",
"type": "int"
}
}
},
"yearCounter":"{count, plural, =1{year} other{years} }",
"@yearCounter":{
"placeholders": {
"count":{
"description": "Specifies how many years are being counted",
"type": "int"
}
}
},
"startingWithDate": "starting",
"@startingWithDate":{
"description": "Shown after 'Recur every X Y', e.g. 'Recur every 2 month starting 20th June 2023'"
}
} }

View file

@ -3,37 +3,25 @@ import 'dart:io';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:logger/logger.dart'; 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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
var _materialYou = false; var _materialYou = false;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
if (!Platform.isAndroid) {
await s.setBool("useMaterialYou", false);
}
_materialYou = s.getBool("useMaterialYou") ?? true; _materialYou = s.getBool("useMaterialYou") ?? true;
runApp(const MyApp()); runApp(const MyApp());
} }
/// Global logger for debugging
final logger = Logger(); final logger = Logger();
/// The application itself
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
/// The application itself
const MyApp({super.key}); const MyApp({super.key});
/// If Material You was applied
///
/// Used to check if it is supported
static bool appliedYou = false; static bool appliedYou = false;
// This widget is the root of your application. // This widget is the root of your application.
@override @override
@ -47,21 +35,21 @@ class MyApp extends StatelessWidget {
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
...GlobalMaterialLocalizations.delegates, ...GlobalMaterialLocalizations.delegates,
...GlobalCupertinoLocalizations.delegates, ...GlobalCupertinoLocalizations.delegates
], ],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
title: 'Prašule', title: 'Prašule',
theme: ThemeData( theme: ThemeData(
colorScheme: _materialYou colorScheme: (_materialYou)
? light ?? lightColorScheme ? light ?? lightColorScheme
: lightColorScheme, : lightColorScheme,
useMaterial3: true, useMaterial3: true,
), ),
darkTheme: ThemeData( darkTheme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: colorScheme: (_materialYou)
_materialYou ? dark ?? darkColorScheme : darkColorScheme, ? dark ?? darkColorScheme
), : darkColorScheme),
home: const HomeView(), home: const HomeView(),
); );
}, },
@ -69,17 +57,11 @@ class MyApp extends StatelessWidget {
: Theme( : Theme(
data: ThemeData( data: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: colorScheme: (MediaQuery.of(context).platformBrightness ==
(MediaQuery.of(context).platformBrightness == Brightness.dark) Brightness.dark)
? darkColorScheme ? darkColorScheme
: lightColorScheme, : lightColorScheme),
),
child: const CupertinoApp( child: const CupertinoApp(
localizationsDelegates: [
AppLocalizations.delegate,
...GlobalMaterialLocalizations.delegates,
...GlobalCupertinoLocalizations.delegates,
],
title: 'Prašule', title: 'Prašule',
home: HomeView(), home: HomeView(),
), ),

View file

@ -4,48 +4,40 @@ import 'package:dio/dio.dart';
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
/// Used for communication with my repo mirror
///
/// Downloads Tessdata for OCR
class TessdataApi { class TessdataApi {
static final Dio _client = Dio( static final Dio _client = Dio(
BaseOptions( BaseOptions(
validateStatus: (status) => true, validateStatus: (status) => true,
), ),
); );
/// Gets available languages from the repo
static Future<List<String>> getAvailableData() async { static Future<List<String>> getAvailableData() async {
final res = await _client.get<List<Map<String, dynamic>>>( var res = await _client.get(
"https://git.mnau.xyz/api/v1/repos/hernik/tessdata_fast/contents", "https://git.mnau.xyz/api/v1/repos/hernik/tessdata_fast/contents",
options: Options(headers: {"Accept": "application/json"}), options: Options(headers: {"Accept": "application/json"}));
);
if ((res.statusCode ?? 500) > 399) { if ((res.statusCode ?? 500) > 399) {
return Future.error("The server returned status code ${res.statusCode}"); return Future.error("The server returned status code ${res.statusCode}");
} }
final data = res.data; var data = res.data;
final dataFiles = <String>[]; final dataFiles = <String>[];
for (final file in data ?? <Map<String, dynamic>>[]) { for (var file in data) {
if (!(file["name"] as String).endsWith(".traineddata")) continue; if (!file["name"].endsWith(".traineddata")) continue;
dataFiles.add((file["name"] as String).replaceAll(".traineddata", "")); dataFiles.add(file["name"].replaceAll(".traineddata", ""));
} }
return dataFiles; return dataFiles;
} }
/// Deletes data from device
static Future<void> deleteData(String name) async { static Future<void> deleteData(String name) async {
final dataDir = Directory(await FlutterTesseractOcr.getTessdataPath()); var dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
if (!dataDir.existsSync()) { if (!dataDir.existsSync()) {
dataDir.createSync(); dataDir.createSync();
} }
final dataFile = File("${dataDir.path}/$name.traineddata"); var dataFile = File("${dataDir.path}/$name.traineddata");
if (!dataFile.existsSync()) return; if (!dataFile.existsSync()) return;
dataFile.deleteSync(); dataFile.deleteSync();
} }
/// Finds existing data on the device
static Future<List<String>> getDownloadedData() async { static Future<List<String>> getDownloadedData() async {
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath()); var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
if (!tessDir.existsSync()) { if (!tessDir.existsSync()) {
tessDir.createSync(); tessDir.createSync();
} }
@ -56,29 +48,25 @@ class TessdataApi {
.toList(); .toList();
} }
/// Downloads data from the repo to the device static Future<void> downloadData(String isoCode,
static Future<void> downloadData( {void Function(int, int)? callback}) async {
String isoCode, { var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
void Function(int, int)? callback,
}) async {
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
if (!tessDir.existsSync()) { if (!tessDir.existsSync()) {
tessDir.createSync(); tessDir.createSync();
} }
final file = File("${tessDir.path}/$isoCode.traineddata"); var file = File("${tessDir.path}/$isoCode.traineddata");
if (file.existsSync()) return; // TODO: maybe ask to redownload? if (file.existsSync()) return; // TODO: maybe ask to redownload?
final res = await _client.get<List<int>>( var res = await _client.get(
"https://git.mnau.xyz/hernik/tessdata_fast/raw/branch/main/$isoCode.traineddata", "https://git.mnau.xyz/hernik/tessdata_fast/raw/branch/main/$isoCode.traineddata",
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
onReceiveProgress: callback, onReceiveProgress: callback);
);
if ((res.statusCode ?? 500) > 399) { if ((res.statusCode ?? 500) > 399) {
return Future.error("The server returned status code ${res.statusCode}"); return Future.error("The server returned status code ${res.statusCode}");
} }
try { try {
file.openSync(mode: FileMode.write) var writefile = file.openSync(mode: FileMode.write);
..writeFromSync(res.data!) writefile.writeFromSync(res.data);
..closeSync(); writefile.closeSync();
} catch (e) { } catch (e) {
logger.e(e); logger.e(e);
return Future.error("Could not complete writing file"); return Future.error("Could not complete writing file");

View file

@ -1,20 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:prasule/pw/platformwidget.dart'; import 'package:prasule/pw/platformwidget.dart';
/// A [PlatformWidget] implementation of a text field
class PlatformButton extends PlatformWidget<TextButton, CupertinoButton> { class PlatformButton extends PlatformWidget<TextButton, CupertinoButton> {
const PlatformButton({
required this.text,
required this.onPressed,
super.key,
this.style,
});
final String text; final String text;
final void Function()? onPressed; final void Function()? onPressed;
final ButtonStyle? style; final ButtonStyle? style;
const PlatformButton(
{super.key, required this.text, required this.onPressed, this.style});
@override @override
TextButton createAndroidWidget(BuildContext context) => TextButton( TextButton createAndroidWidget(BuildContext context) => TextButton(

View file

@ -1,16 +1,13 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:prasule/pw/platformwidget.dart'; import 'package:prasule/pw/platformwidget.dart';
/// A [PlatformWidget] implementation of a dialog
class PlatformDialog extends PlatformWidget<AlertDialog, CupertinoAlertDialog> { class PlatformDialog extends PlatformWidget<AlertDialog, CupertinoAlertDialog> {
const PlatformDialog(
{required this.title, super.key, this.content, this.actions = const [],});
final String title; final String title;
final Widget? content; final Widget? content;
final List<Widget> actions; final List<Widget> actions;
const PlatformDialog(
{super.key, required this.title, this.content, this.actions = const []});
@override @override
AlertDialog createAndroidWidget(BuildContext context) => AlertDialog( AlertDialog createAndroidWidget(BuildContext context) => AlertDialog(

View file

@ -1,27 +1,9 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:prasule/pw/platformwidget.dart'; import 'package:prasule/pw/platformwidget.dart';
/// A [PlatformWidget] implementation of a text field
class PlatformField extends PlatformWidget<TextField, CupertinoTextField> { class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
const PlatformField({
super.key,
this.controller,
this.enabled,
this.labelText,
this.obscureText = false,
this.autocorrect = false,
this.keyboardType,
this.inputFormatters = const [],
this.onChanged,
this.autofillHints,
this.textStyle,
this.textAlign = TextAlign.start,
this.maxLines = 1,
});
final TextEditingController? controller; final TextEditingController? controller;
final bool? enabled; final bool? enabled;
final bool obscureText; final bool obscureText;
@ -34,6 +16,20 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
final TextStyle? textStyle; final TextStyle? textStyle;
final TextAlign textAlign; final TextAlign textAlign;
final int? maxLines; final int? maxLines;
const PlatformField(
{super.key,
this.controller,
this.enabled,
this.labelText,
this.obscureText = false,
this.autocorrect = false,
this.keyboardType,
this.inputFormatters = const [],
this.onChanged,
this.autofillHints,
this.textStyle,
this.textAlign = TextAlign.start,
this.maxLines = 1});
@override @override
TextField createAndroidWidget(BuildContext context) => TextField( TextField createAndroidWidget(BuildContext context) => TextField(
@ -43,8 +39,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
obscureText: obscureText, obscureText: obscureText,
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText, labelText: labelText,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(4))),
),
autocorrect: autocorrect, autocorrect: autocorrect,
keyboardType: keyboardType, keyboardType: keyboardType,
style: textStyle, style: textStyle,
@ -61,7 +56,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
controller: controller, controller: controller,
enabled: enabled ?? true, enabled: enabled ?? true,
obscureText: obscureText, obscureText: obscureText,
placeholder: labelText, prefix: (labelText == null) ? null : Text(labelText!),
autocorrect: autocorrect, autocorrect: autocorrect,
keyboardType: keyboardType, keyboardType: keyboardType,
inputFormatters: inputFormatters, inputFormatters: inputFormatters,

View file

@ -3,10 +3,7 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Creates a PageRoute based on [Platform] Route<T> platformRoute<T>(Widget Function(BuildContext) builder) =>
Route<T> platformRoute<T extends Object?>(
Widget Function(BuildContext) builder,
) =>
(Platform.isIOS) (Platform.isIOS)
? CupertinoPageRoute<T>(builder: builder) ? CupertinoPageRoute<T>(builder: builder)
: MaterialPageRoute<T>(builder: builder); : MaterialPageRoute<T>(builder: builder);

View file

@ -5,8 +5,6 @@ import 'package:flutter/material.dart';
/// Abstract class used to create widgets for the respective platform UI library /// Abstract class used to create widgets for the respective platform UI library
abstract class PlatformWidget<A extends Widget, I extends Widget> abstract class PlatformWidget<A extends Widget, I extends Widget>
extends StatelessWidget { extends StatelessWidget {
/// Abstract class used to create widgets
/// for the respective platform UI library
const PlatformWidget({super.key}); const PlatformWidget({super.key});
@override @override
@ -18,9 +16,7 @@ abstract class PlatformWidget<A extends Widget, I extends Widget>
} }
} }
/// The widget that will be shown on Android
A createAndroidWidget(BuildContext context); A createAndroidWidget(BuildContext context);
/// The widget that will be shown on iOS
I createIosWidget(BuildContext context); I createIosWidget(BuildContext context);
} }

View file

@ -1,5 +1,3 @@
// ignore_for_file: public_member_api_docs
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const lightColorScheme = ColorScheme( const lightColorScheme = ColorScheme(

View file

@ -3,7 +3,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/views/graph_view.dart'; import 'package:prasule/views/graph_view.dart';
import 'package:prasule/views/home.dart'; import 'package:prasule/views/home.dart';
import 'package:prasule/views/recurring_view.dart';
/// Makes the drawer because I won't enter the same code in every view /// Makes the drawer because I won't enter the same code in every view
Drawer makeDrawer(BuildContext context, int page) => Drawer( Drawer makeDrawer(BuildContext context, int page) => Drawer(
@ -40,22 +39,6 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
.pushReplacement(platformRoute((p0) => const GraphView())); .pushReplacement(platformRoute((p0) => const GraphView()));
}, },
), ),
ListTile(
leading: const Icon(Icons.repeat),
title: Text(
AppLocalizations.of(context).recurringPayments,
),
selected: page == 3,
onTap: () {
if (page == 3) {
Navigator.of(context).pop();
return;
}
Navigator.of(context).pushReplacement(
platformRoute((p0) => const RecurringEntriesView()),
);
},
),
], ],
), ),
); );

View file

@ -1,8 +0,0 @@
/// Extension to get last day of the month
extension LastDay on DateTime {
/// Returns the last day of the month as [int]
int get lastDay {
final d = add(const Duration(days: 31));
return DateTime(d.year, d.month, 0).day;
}
}

View file

@ -2,67 +2,28 @@ import 'package:currency_picker/currency_picker.dart';
import 'package:dynamic_color/dynamic_color.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:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/api/wallet_entry.dart';
import 'package:prasule/util/get_last_date.dart';
import 'package:prasule/util/text_color.dart';
/// Monthly/Yearly expense/income [LineChart] /// Monthly/Yearly expense/income [LineChart]
class ExpensesLineChart extends StatelessWidget { class ExpensesLineChart extends StatelessWidget {
/// Monthly/Yearly expense/income [LineChart] const ExpensesLineChart(
const ExpensesLineChart({ {super.key,
required this.date, required this.date,
required this.locale, required this.locale,
required this.expenseData, required this.expenseData,
required this.incomeData, required this.incomeData,
required this.currency, required this.currency,
super.key, this.yearly = false});
this.yearly = false,
});
/// If the graph will be shown yearly
final bool yearly; final bool yearly;
/// Selected date
///
/// Used to get either month or year
final DateTime date; final DateTime date;
/// Current locale
///
/// Used mainly for formatting
final String locale; final String locale;
/// The expense data used for the graph
final List<double> expenseData; final List<double> expenseData;
/// Wallet currency
///
/// Used to show currency symbol
final Currency currency; final Currency currency;
List<double> get expenseDataSorted {
/// Expense data, but sorted var list = List<double>.from(expenseData);
List<double> get expenseDataSorted => list.sort((a, b) => a.compareTo(b));
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b)); return list;
/// Income data used for the graph
final List<double> incomeData;
/// Income data, but sorted
List<double> get incomeDataSorted =>
List<double>.from(incomeData)..sort((a, b) => a.compareTo(b));
/// Calculates maxY for the graph
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;
}
} }
final List<double> incomeData; final List<double> incomeData;
@ -91,71 +52,46 @@ class ExpensesLineChart extends StatelessWidget {
getTooltipItems: (spots) => List<LineTooltipItem>.generate( getTooltipItems: (spots) => List<LineTooltipItem>.generate(
spots.length, spots.length,
(index) => LineTooltipItem( (index) => LineTooltipItem(
// Changes what's rendered on the tooltip (spots[index].barIndex == 0)
// when clicked in the chart
(spots[index].barIndex == 0) // income chart
? (yearly ? (yearly
? AppLocalizations.of(context).incomeForMonth( ? AppLocalizations.of(context).incomeForMonth(
DateFormat.MMMM(locale).format( DateFormat.MMMM(locale).format(DateTime(
DateTime( date.year, spots[index].x.toInt() + 1, 1)),
date.year,
spots[index].x.toInt() + 1,
),
),
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(spots[index].y), .format(spots[index].y))
)
: AppLocalizations.of(context).incomeForDay( : AppLocalizations.of(context).incomeForDay(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(spots[index].y), .format(spots[index].y),
)) ))
: (yearly // expense chart : (yearly
? AppLocalizations.of(context).expensesForMonth( ? AppLocalizations.of(context).expensesForMonth(
DateFormat.MMMM(locale).format( DateFormat.MMMM(locale).format(DateTime(
DateTime( date.year, spots[index].x.toInt() + 1, 1)),
date.year,
spots[index].x.toInt() + 1,
),
),
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(spots[index].y), .format(spots[index].y))
)
: AppLocalizations.of(context).expensesForDay( : AppLocalizations.of(context).expensesForDay(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(spots[index].y), .format(spots[index].y),
)), )),
TextStyle(color: spots[index].bar.color), TextStyle(color: spots[index].bar.color),
children: [
TextSpan(
text: "\n${yearly ? DateFormat.MMMM(locale).format(
DateTime(
date.year,
index + 1,
),
) : DateFormat.yMMMMd(locale).format(DateTime(date.year, date.month, spots[index].spotIndex + 1))}",
),
],
), ),
), ),
), ),
), ),
maxY: maxY, maxY: maxY,
maxX: yearly maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
? 12
: date.lastDay.toDouble() -
1, // remove 1 because we are indexing from 0
minY: 0, minY: 0,
minX: 0, minX: 0,
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
@ -166,14 +102,11 @@ class ExpensesLineChart extends StatelessWidget {
barWidth: 8, barWidth: 8,
isStrokeCapRound: true, isStrokeCapRound: true,
dotData: const FlDotData(show: false), dotData: const FlDotData(show: false),
belowBarData: BarAreaData(), belowBarData: BarAreaData(show: false),
color: color: Colors.green
(MediaQuery.of(context).platformBrightness == Brightness.dark) .harmonizeWith(Theme.of(context).colorScheme.secondary),
? Colors.green.shade300
: Colors.green
.harmonizeWith(Theme.of(context).colorScheme.primary),
spots: List.generate( spots: List.generate(
yearly ? 12 : date.lastDay, (yearly) ? 12 : DateTime(date.year, date.month, 0).day,
(index) => FlSpot(index.toDouble(), incomeData[index]), (index) => FlSpot(index.toDouble(), incomeData[index]),
), ),
), ),
@ -183,37 +116,21 @@ class ExpensesLineChart extends StatelessWidget {
barWidth: 8, barWidth: 8,
isStrokeCapRound: true, isStrokeCapRound: true,
dotData: const FlDotData(show: false), dotData: const FlDotData(show: false),
belowBarData: BarAreaData(), belowBarData: BarAreaData(show: false),
color: color: Colors.red
(MediaQuery.of(context).platformBrightness == Brightness.dark) .harmonizeWith(Theme.of(context).colorScheme.secondary),
? Colors.red.shade300
: Colors.red
.harmonizeWith(Theme.of(context).colorScheme.primary),
spots: List.generate( spots: List.generate(
yearly (yearly) ? 12 : DateTime(date.year, date.month, 0).day,
? 12 (index) => FlSpot(index.toDouble() + 1, expenseData[index]),
: date.lastDay, // no -1 because it's the length, not index
(index) => FlSpot(index.toDouble(), expenseData[index]),
), ),
), ),
], // actual data ], // actual data
titlesData: FlTitlesData( titlesData: FlTitlesData(
rightTitles: const AxisTitles(), rightTitles: const AxisTitles(
topTitles: const AxisTitles(), sideTitles: SideTitles(showTitles: false),
leftTitles: AxisTitles(
sideTitles: SideTitles(
reservedSize: (NumberFormat.compact()
.format(expenseDataSorted.last)
.length >=
5 ||
NumberFormat.compact()
.format(incomeDataSorted.last)
.length >=
5)
? 50
: 25,
showTitles: true,
), ),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
), ),
bottomTitles: AxisTitles( bottomTitles: AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
@ -223,15 +140,13 @@ class ExpensesLineChart extends StatelessWidget {
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() + 1).toString(); text = (value.toInt() + 1).toString();
} }
return SideTitleWidget( return SideTitleWidget(
axisSide: meta.axisSide, axisSide: meta.axisSide, child: Text(text));
child: Text(text),
);
}, },
), ),
), ),
@ -241,52 +156,34 @@ class ExpensesLineChart extends StatelessWidget {
} }
} }
/// Renders expenses/income as a [BarChart]
class ExpensesBarChart extends StatelessWidget { class ExpensesBarChart extends StatelessWidget {
/// Renders expenses/income as a [BarChart] const ExpensesBarChart(
const ExpensesBarChart({ {super.key,
required this.yearly, required this.yearly,
required this.date, required this.date,
required this.locale, required this.locale,
required this.expenseData, required this.expenseData,
required this.incomeData, required this.incomeData,
required this.currency, required this.currency});
super.key,
});
/// If the graph will be shown yearly
final bool yearly; final bool yearly;
/// Selected date
///
/// Used to get either month or year
final DateTime date; final DateTime date;
/// Current locale
///
/// Used mainly for formatting
final String locale; final String locale;
/// The expense data used for the graph
final List<double> expenseData; final List<double> expenseData;
List<double> get expenseDataSorted {
var list = List<double>.from(expenseData);
list.sort((a, b) => a.compareTo(b));
return list;
}
/// Wallet currency
///
/// Used to show currency symbol
final Currency currency; final Currency currency;
/// Expense data, but sorted
List<double> get expenseDataSorted =>
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
/// Income data used for the graph
final List<double> incomeData; final List<double> incomeData;
List<double> get incomeDataSorted {
var list = List<double>.from(incomeData);
list.sort((a, b) => a.compareTo(b));
return list;
}
/// Income data, but sorted
List<double> get incomeDataSorted =>
List<double>.from(incomeData)..sort((a, b) => a.compareTo(b));
/// Calculates maxY for the graph
double get maxY { double get maxY {
if (incomeData.isEmpty) return expenseDataSorted.last; if (incomeData.isEmpty) return expenseDataSorted.last;
if (expenseData.isEmpty) return incomeDataSorted.last; if (expenseData.isEmpty) return incomeDataSorted.last;
@ -304,28 +201,26 @@ class ExpensesBarChart extends StatelessWidget {
enabled: true, enabled: true,
touchTooltipData: BarTouchTooltipData( touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem: (group, groupIndex, rod, rodIndex) =>
yearly // create custom tooltips for graph bars (yearly) // create custom tooltips for graph bars
? BarTooltipItem( ? BarTooltipItem(
(rodIndex == 1) (rodIndex == 1)
? AppLocalizations.of(context).expensesForMonth( ? AppLocalizations.of(context).expensesForMonth(
DateFormat.MMMM(locale).format( DateFormat.MMMM(locale).format(
DateTime(date.year, groupIndex + 1), DateTime(date.year, groupIndex + 1, 1)),
),
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(rod.toY), .format(rod.toY),
) )
: AppLocalizations.of(context).incomeForMonth( : AppLocalizations.of(context).incomeForMonth(
DateFormat.MMMM(locale).format( DateFormat.MMMM(locale).format(
DateTime(date.year, groupIndex + 1), DateTime(date.year, groupIndex + 1, 1)),
),
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(rod.toY), .format(rod.toY),
), ),
TextStyle(color: rod.color), TextStyle(color: rod.color),
) )
@ -335,23 +230,27 @@ class ExpensesBarChart extends StatelessWidget {
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(rod.toY), .format(rod.toY),
) )
: AppLocalizations.of(context).incomeForDay( : AppLocalizations.of(context).incomeForDay(
NumberFormat.compactCurrency( NumberFormat.compactCurrency(
locale: locale, locale: locale,
symbol: currency.symbol, symbol: currency.symbol,
name: currency.name, name: currency.name)
).format(rod.toY), .format(rod.toY),
), ),
TextStyle(color: rod.color), TextStyle(color: rod.color),
), ),
), ),
), ),
titlesData: FlTitlesData( titlesData: FlTitlesData(
rightTitles: const AxisTitles(), rightTitles: const AxisTitles(
topTitles: const AxisTitles(), sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles( bottomTitles: AxisTitles(
sideTitles: SideTitles( sideTitles: SideTitles(
showTitles: true, showTitles: true,
@ -360,15 +259,13 @@ class ExpensesBarChart extends StatelessWidget {
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() + 1).toString(); text = (value.toInt() + 1).toString();
} }
return SideTitleWidget( return SideTitleWidget(
axisSide: meta.axisSide, axisSide: meta.axisSide, child: Text(text));
child: Text(text),
);
}, },
), ),
), ),
@ -376,7 +273,7 @@ class ExpensesBarChart extends StatelessWidget {
minY: 0, minY: 0,
maxY: maxY, maxY: maxY,
barGroups: List<BarChartGroupData>.generate( barGroups: List<BarChartGroupData>.generate(
yearly ? 12 : date.lastDay - 1, (yearly) ? 12 : DateTime(date.year, date.month, 0).day,
(index) => BarChartGroupData( (index) => BarChartGroupData(
x: index, x: index,
barRods: [ barRods: [
@ -384,13 +281,13 @@ class ExpensesBarChart extends StatelessWidget {
BarChartRodData( BarChartRodData(
toY: incomeData[index], toY: incomeData[index],
color: Colors.green color: Colors.green
.harmonizeWith(Theme.of(context).colorScheme.primary), .harmonizeWith(Theme.of(context).colorScheme.secondary),
), ),
if (expenseData.isNotEmpty) if (expenseData.isNotEmpty)
BarChartRodData( BarChartRodData(
toY: expenseData[index], toY: expenseData[index],
color: Colors.red color: Colors.red
.harmonizeWith(Theme.of(context).colorScheme.primary), .harmonizeWith(Theme.of(context).colorScheme.secondary),
), ),
], ],
), ),
@ -398,165 +295,3 @@ class ExpensesBarChart extends StatelessWidget {
), ),
); );
} }
/// [PieChart] used to display expenses/income visualized
/// under their respective category
class CategoriesPieChart extends StatefulWidget {
/// [PieChart] used to display expenses/income visualized
/// under their respective category
const CategoriesPieChart({
required this.entries,
required this.categories,
required this.symbol,
super.key,
});
/// Entries to be used
final List<WalletSingleEntry> entries;
/// Categories to be displayed
final List<WalletCategory> categories;
/// Currency symbol displayed on the chart
final String symbol;
@override
State<CategoriesPieChart> createState() => _CategoriesPieChartState();
}
class _CategoriesPieChartState extends State<CategoriesPieChart> {
int touchedIndex = -1;
@override
Widget build(BuildContext context) => Column(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
child: Wrap(
alignment: WrapAlignment.center,
spacing: 4,
children: List<Widget>.generate(
widget.categories.length,
(index) => Padding(
padding: const EdgeInsets.all(8),
child: Indicator(
size: touchedIndex == index ? 18 : 16,
color: widget.categories[index].color,
text: widget.categories[index].name,
textStyle: TextStyle(
fontWeight: (touchedIndex == index)
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
),
),
const SizedBox(
height: 5,
),
Expanded(
child: PieChart(
PieChartData(
centerSpaceRadius: double.infinity,
pieTouchData: PieTouchData(
touchCallback: (event, response) {
// Set touchedIndex so we can highlight
// the corresponding indicator
setState(() {
if (!event.isInterestedForInteractions ||
response == null ||
response.touchedSection == null) {
touchedIndex = -1;
return;
}
touchedIndex =
response.touchedSection!.touchedSectionIndex;
});
},
),
sections: List<PieChartSectionData>.generate(
widget.categories.length,
(index) => PieChartSectionData(
title: NumberFormat.compactCurrency(symbol: widget.symbol)
.format(
widget.entries
.where(
(element) =>
element.category.id ==
widget.categories[index].id,
)
.fold<double>(
0,
(previousValue, element) =>
previousValue + element.data.amount,
),
),
titleStyle: TextStyle(
color:
widget.categories[index].color.calculateTextColor(),
fontWeight: FontWeight.bold,
),
color: widget.categories[index].color,
value: widget.entries
.where(
(element) =>
element.category.id ==
widget.categories[index].id,
)
.fold<double>(
0,
(previousValue, element) =>
previousValue + element.data.amount,
),
),
),
),
),
),
],
);
}
/// Used to indicate which part of a chart is for what
class Indicator extends StatelessWidget {
/// Used to indicate which part of a chart is for what
const Indicator({
required this.size,
required this.color,
required this.text,
this.textStyle = const TextStyle(),
super.key,
});
/// Size of the indicator circle
final double size;
/// Color of the indicator circle
final Color color;
/// Text shown next to the indicator circle
final String text;
final TextStyle textStyle;
@override
Widget build(BuildContext context) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
const SizedBox(
width: 4,
),
Text(text, style: textStyle),
],
);
}

View file

@ -1,15 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
/// Shows either SnackBar on Android or Toast on iOS
Future<void> showMessage(String message, BuildContext context) async {
if (Platform.isIOS) {
await Fluttertoast.showToast(msg: message, toastLength: Toast.LENGTH_LONG);
} else {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
}

View file

@ -1,11 +0,0 @@
import 'package:flutter/material.dart';
/// Used to add [calculateTextColor] to the [Color] class
extension TextColor on Color {
/// Returns if foreground should be white or dark on this [Color]
Color calculateTextColor() {
return ThemeData.estimateBrightnessForColor(this) == Brightness.light
? Colors.black
: Colors.white;
}
}

View file

@ -1,33 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformfield.dart';
import 'package:prasule/util/show_message.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Used when user wants to add new entry class CreateEntryView extends StatefulWidget {
class CreateSingleEntryView extends StatefulWidget {
/// Used when user wants to add new entry
const CreateSingleEntryView({required this.w, super.key, this.editEntry});
/// The wallet, where the entry will be saved to
final Wallet w; final Wallet w;
/// Entry we want to edit
///
/// Is null unless we are editing an existing entry
final WalletSingleEntry? editEntry; final WalletSingleEntry? editEntry;
const CreateEntryView({super.key, required this.w, this.editEntry});
@override @override
State createState() => _CreateSingleEntryViewState(); State<CreateEntryView> createState() => _CreateEntryViewState();
} }
class _CreateSingleEntryViewState extends State<CreateSingleEntryView> { class _CreateEntryViewState extends State<CreateEntryView> {
late WalletSingleEntry newEntry; late WalletSingleEntry newEntry;
@override @override
void initState() { void initState() {
@ -40,8 +31,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
data: EntryData(amount: 0, name: ""), data: EntryData(amount: 0, name: ""),
type: EntryType.expense, type: EntryType.expense,
date: DateTime.now(), date: DateTime.now(),
category: widget.w.categories.first, category: widget.w.categories.first);
);
} }
setState(() {}); setState(() {});
} }
@ -78,14 +68,12 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
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:
const TextInputType.numberWithOptions(decimal: true), const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(
RegExp(r'\d+[\.,]{0,1}\d{0,}'), RegExp(r'\d+[\.,]{0,1}\d{0,}'))
),
], ],
onChanged: (v) { onChanged: (v) {
newEntry.data.amount = double.parse(v); newEntry.data.amount = double.parse(v);
@ -171,8 +159,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
constraints: BoxConstraints( constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width * 0.8, minWidth: MediaQuery.of(context).size.width * 0.8,
maxWidth: MediaQuery.of(context).size.width * 0.8, maxWidth: MediaQuery.of(context).size.width * 0.8,
maxHeight: 300, maxHeight: 300),
),
child: PlatformField( child: PlatformField(
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
@ -191,8 +178,13 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
text: AppLocalizations.of(context).save, text: AppLocalizations.of(context).save,
onPressed: () { onPressed: () {
if (newEntry.data.name.isEmpty) { if (newEntry.data.name.isEmpty) {
showMessage( ScaffoldMessenger.of(context).clearSnackBars();
AppLocalizations.of(context).errorEmptyName, context); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).errorEmptyName),
),
);
return; return;
} }
if (widget.editEntry != null) { if (widget.editEntry != null) {
@ -204,7 +196,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
(value) => Navigator.of(context).pop(widget.w), (value) => Navigator.of(context).pop(widget.w),
); // TODO loading circle? ); // TODO loading circle?
}, },
), )
], ],
), ),
), ),

View file

@ -1,338 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/recurring_entry.dart';
import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart';
import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformfield.dart';
import 'package:prasule/util/show_message.dart';
/// Used when user wants to add new entry
class CreateRecurringEntryView extends StatefulWidget {
/// Used when user wants to add new entry
const CreateRecurringEntryView({
required this.w,
required this.locale,
super.key,
this.editEntry,
});
/// The wallet, where the entry will be saved to
final Wallet w;
/// Entry we want to edit
///
/// Is null unless we are editing an existing entry
final RecurringWalletEntry? editEntry;
/// Selected locale
final String locale;
@override
State createState() => _CreateRecurringEntryViewState();
}
class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
late RecurringWalletEntry newEntry;
@override
void initState() {
super.initState();
if (widget.editEntry != null) {
newEntry = widget.editEntry!;
} else {
newEntry = RecurringWalletEntry(
id: widget.w.nextId,
data: EntryData(amount: 0, name: ""),
type: EntryType.expense,
date: DateTime.now(),
category: widget.w.categories.first,
lastRunDate: DateTime.now(),
recurType: RecurType.month,
);
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).createEntry),
),
body: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField(
labelText: AppLocalizations.of(context).name,
controller: TextEditingController(text: newEntry.data.name),
onChanged: (v) {
newEntry.data.name = v;
},
),
),
const SizedBox(
height: 15,
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: PlatformField(
labelText: AppLocalizations.of(context).amount,
controller: TextEditingController(
text: newEntry.data.amount.toString(),
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
),
],
onChanged: (v) {
logger.i(v);
newEntry.data.amount = double.parse(v);
},
),
),
const SizedBox(
height: 20,
),
Text(AppLocalizations.of(context).type),
const SizedBox(
height: 10,
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: DropdownButton<EntryType>(
value: newEntry.type,
items: [
DropdownMenuItem(
value: EntryType.expense,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 24,
child: Text(
AppLocalizations.of(context).expense,
),
),
),
DropdownMenuItem(
value: EntryType.income,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 24,
child: Text(AppLocalizations.of(context).income),
),
),
],
onChanged: (v) {
if (v == null) return;
newEntry.type = v;
setState(() {});
},
),
),
const SizedBox(
height: 20,
),
Text(AppLocalizations.of(context).category),
const SizedBox(
height: 10,
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.8,
child: DropdownButton<int>(
value: newEntry.category.id,
items: List.generate(
widget.w.categories.length,
(index) => DropdownMenuItem(
value: widget.w.categories[index].id,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.8 - 24,
child: Text(
widget.w.categories[index].name,
),
),
),
),
onChanged: (v) {
if (v == null) return;
newEntry.category = widget.w.categories
.where((element) => element.id == v)
.first;
setState(() {});
},
),
),
const SizedBox(
height: 20,
),
Text(AppLocalizations.of(context).description),
const SizedBox(
height: 10,
),
ConstrainedBox(
constraints: BoxConstraints(
minWidth: MediaQuery.of(context).size.width * 0.8,
maxWidth: MediaQuery.of(context).size.width * 0.8,
maxHeight: 300,
),
child: PlatformField(
keyboardType: TextInputType.multiline,
maxLines: null,
controller: TextEditingController(
text: newEntry.data.description,
),
onChanged: (v) {
newEntry.data.description = v;
},
),
),
const SizedBox(
height: 20,
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context)
.recurEvery(newEntry.repeatAfter),
),
const SizedBox(
width: 10,
),
SizedBox(
width: 50,
child: PlatformField(
controller: TextEditingController(
text: newEntry.repeatAfter.toString(),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
FilteringTextInputFormatter.deny(
RegExp(r"^0$"),
replacementString: "1",
),
FilteringTextInputFormatter.deny(
r"\d+[\.,]{0,1}\d{0,}",
replacementString: "1",
),
],
onChanged: (s) {
final n = int.tryParse(s);
if (n == null) return;
newEntry.repeatAfter = n;
setState(() {});
},
),
),
],
),
),
SizedBox(
width: 200,
child: DropdownButton<RecurType>(
value: newEntry.recurType,
items: [
DropdownMenuItem(
value: RecurType.day,
child: SizedBox(
width: 176,
child: Text(
AppLocalizations.of(context)
.dayCounter(newEntry.repeatAfter),
),
),
),
DropdownMenuItem(
value: RecurType.month,
child: SizedBox(
width: 176,
child: Text(
AppLocalizations.of(context)
.monthCounter(newEntry.repeatAfter),
),
),
),
DropdownMenuItem(
value: RecurType.year,
child: SizedBox(
width: 176,
child: Text(
AppLocalizations.of(context)
.yearCounter(newEntry.repeatAfter),
),
),
),
],
onChanged: (v) {
if (v == null) return;
newEntry.recurType = v;
setState(() {});
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(AppLocalizations.of(context).startingWithDate),
const SizedBox(
width: 10,
), // TODO: maybe use sizedbox on row with spaceEvenly?
PlatformButton(
text: DateFormat.yMMMMd(widget.locale)
.format(newEntry.lastRunDate),
onPressed: () async {
final d = await showDatePicker(
context: context,
firstDate: DateTime.now(),
lastDate:
DateTime.now().add(const Duration(days: 365)),
);
if (d == null) return;
newEntry.lastRunDate = d;
setState(() {});
},
),
],
),
const SizedBox(
height: 15,
),
PlatformButton(
text: AppLocalizations.of(context).save,
onPressed: () {
if (newEntry.data.name.isEmpty) {
showMessage(
AppLocalizations.of(context).errorEmptyName, context);
return;
}
if (widget.editEntry != null) {
Navigator.of(context).pop(newEntry);
return;
}
widget.w.recurringEntries.add(newEntry);
WalletManager.saveWallet(widget.w).then(
(value) => Navigator.of(context).pop(widget.w),
); // TODO loading circle?
},
),
],
),
),
),
),
);
}
}

View file

@ -1,23 +1,19 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/drawer.dart'; import 'package:prasule/util/drawer.dart';
import 'package:prasule/util/graphs.dart'; 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/settings/settings.dart';
import 'package:prasule/views/setup.dart'; import 'package:prasule/views/setup.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Shows data from a [Wallet] in graphs
class GraphView extends StatefulWidget { class GraphView extends StatefulWidget {
/// Shows data from a [Wallet] in graphs
const GraphView({super.key}); const GraphView({super.key});
@override @override
@ -29,8 +25,8 @@ class _GraphViewState extends State<GraphView> {
Wallet? selectedWallet; Wallet? selectedWallet;
List<Wallet> wallets = []; List<Wallet> wallets = [];
String? locale; String? locale;
Set<String> yearlyBtnSet = {"monthly"}; var yearlyBtnSet = {"monthly"};
Set<String> graphTypeSet = {"expense", "income"}; var graphTypeSet = {"expense", "income"};
bool get yearly => yearlyBtnSet.contains("yearly"); bool get yearly => yearlyBtnSet.contains("yearly");
@override @override
@ -40,25 +36,23 @@ class _GraphViewState extends State<GraphView> {
} }
List<double> generateChartData(EntryType type) { List<double> generateChartData(EntryType type) {
final d = _selectedDate.add(const Duration(days: 31)); var data = List<double>.filled(
final data = List<double>.filled( (yearly)
yearly ? 12 : DateTime(d.year, d.month, 0).day, ? 12
0, : DateTime(_selectedDate.year, _selectedDate.month, 0).day,
); 0.0);
if (selectedWallet == null) return []; if (selectedWallet == null) return [];
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
final entriesForRange = selectedWallet!.entries.where( var entriesForRange = selectedWallet!.entries.where((element) =>
(element) =>
((!yearly) ((!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, element.type == type);
);
var sum = 0.0; var sum = 0.0;
for (final e in entriesForRange) { for (var e in entriesForRange) {
sum += e.data.amount; sum += e.data.amount;
} }
data[i] = sum; data[i] = sum;
@ -66,13 +60,11 @@ class _GraphViewState extends State<GraphView> {
return data; return data;
} }
Future<void> loadWallet() async { void loadWallet() async {
wallets = await WalletManager.listWallets(); wallets = await WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( Navigator.of(context).pushReplacement(
Navigator.of(context) MaterialPageRoute(builder: (c) => const SetupView()));
.pushReplacement(platformRoute((c) => const SetupView())),
);
return; return;
} }
selectedWallet = wallets.first; selectedWallet = wallets.first;
@ -93,47 +85,6 @@ class _GraphViewState extends State<GraphView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
floatingActionButton: Tooltip(
message: AppLocalizations.of(context).changeDate,
child: PlatformButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.primary,
),
foregroundColor: MaterialStateProperty.all(
Theme.of(context).colorScheme.onPrimary,
),
),
text: yearly
? DateFormat.y(locale).format(_selectedDate)
: DateFormat.yMMMM(locale).format(_selectedDate),
onPressed: () async {
final firstDate = (selectedWallet!.entries
..sort(
(a, b) => a.date.compareTo(b.date),
))
.first
.date;
final newDate = await showDatePicker(
context: context,
initialDate: DateTime(
_selectedDate.year,
_selectedDate.month,
),
firstDate: firstDate,
lastDate: DateTime.now(),
initialEntryMode: yearly
? DatePickerEntryMode.input
: DatePickerEntryMode.calendar,
initialDatePickerMode:
yearly ? DatePickerMode.year : DatePickerMode.day,
);
if (newDate == null) return;
_selectedDate = newDate;
setState(() {});
},
),
),
appBar: AppBar( appBar: AppBar(
title: DropdownButton<int>( title: DropdownButton<int>(
value: value:
@ -150,7 +101,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 {
if (v == null || v == -1) { if (v == null || v == -1) {
@ -175,32 +126,23 @@ class _GraphViewState extends State<GraphView> {
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) Navigator.of(context).push(
.push( MaterialPageRoute(
platformRoute( builder: (context) => const SettingsView(),
(context) => const SettingsView(),
), ),
) );
.then((value) async {
selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name);
final s = await SharedPreferences.getInstance();
chartType = s.getInt("monthlygraph") ?? 2;
setState(() {});
});
} else if (value == AppLocalizations.of(context).about) { } 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");
);
} }
}, },
), )
], ],
), ),
drawer: makeDrawer(context, 2), drawer: makeDrawer(context, 2),
@ -251,8 +193,8 @@ class _GraphViewState extends State<GraphView> {
selected: yearlyBtnSet, selected: yearlyBtnSet,
onSelectionChanged: (selection) async { onSelectionChanged: (selection) async {
yearlyBtnSet = selection; yearlyBtnSet = selection;
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
chartType = yearly chartType = (yearly)
? (s.getInt("yearlygraph") ?? 1) ? (s.getInt("yearlygraph") ?? 1)
: (s.getInt("monthlygraph") ?? 2); : (s.getInt("monthlygraph") ?? 2);
setState(() {}); setState(() {});
@ -262,17 +204,61 @@ class _GraphViewState extends State<GraphView> {
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
color: color: Theme.of(context)
Theme.of(context).colorScheme.secondaryContainer, .colorScheme
), .secondaryContainer),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ 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( SizedBox(
width: MediaQuery.of(context).size.width * 0.9, width: MediaQuery.of(context).size.width * 0.9,
height: height: 300,
MediaQuery.of(context).size.height * 0.35,
child: (chartType == null) child: (chartType == null)
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: (chartType == 1) : (chartType == 1)
@ -284,63 +270,35 @@ class _GraphViewState extends State<GraphView> {
expenseData: (graphTypeSet expenseData: (graphTypeSet
.contains("expense")) .contains("expense"))
? generateChartData( ? generateChartData(
EntryType.expense, EntryType.expense)
)
: [], : [],
incomeData: (graphTypeSet incomeData: (graphTypeSet
.contains("income")) .contains("income"))
? generateChartData( ? generateChartData(
EntryType.income, EntryType.income)
)
: [], : [],
) )
: Padding( : ExpensesLineChart(
padding: const EdgeInsets.all(8), currency: selectedWallet!.currency,
child: ExpensesLineChart(
currency:
selectedWallet!.currency,
date: _selectedDate, date: _selectedDate,
locale: locale ?? "en", locale: locale ?? "en",
yearly: yearly, yearly: yearly,
expenseData: (graphTypeSet expenseData: (graphTypeSet
.contains("expense")) .contains("expense"))
? generateChartData( ? generateChartData(
EntryType.expense, EntryType.expense)
)
: [], : [],
incomeData: (graphTypeSet incomeData: (graphTypeSet
.contains("income")) .contains("income"))
? generateChartData( ? generateChartData(
EntryType.income, EntryType.income)
)
: [], : [],
), ),
), )
),
], ],
), ),
), ),
), )
const SizedBox(
height: 25,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color:
Theme.of(context).colorScheme.secondaryContainer,
),
width: MediaQuery.of(context).size.width * 0.95,
height: MediaQuery.of(context).size.height * 0.35,
child: Padding(
padding: const EdgeInsets.all(8),
child: CategoriesPieChart(
symbol: selectedWallet!.currency.symbol,
entries: selectedWallet!.entries,
categories: selectedWallet!.categories,
),
),
),
], ],
), ),
), ),

View file

@ -1,12 +1,6 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
@ -16,25 +10,22 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart'; import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/recurring_entry.dart'; import 'package:prasule/api/walletentry.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_entry.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
import 'package:prasule/network/tessdata.dart'; import 'package:prasule/network/tessdata.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformdialog.dart';
import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/drawer.dart'; import 'package:prasule/util/drawer.dart';
import 'package:prasule/util/text_color.dart';
import 'package:prasule/views/create_entry.dart'; import 'package:prasule/views/create_entry.dart';
import 'package:prasule/views/settings/settings.dart'; import 'package:prasule/views/settings/settings.dart';
import 'package:prasule/views/settings/tessdata_list.dart'; import 'package:prasule/views/settings/tessdata_list.dart';
import 'package:prasule/views/setup.dart'; import 'package:prasule/views/setup.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Main view, shows entries
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
/// Main view, shows entries
const HomeView({super.key}); const HomeView({super.key});
@override @override
@ -59,17 +50,14 @@ class _HomeViewState extends State<HomeView> {
loadWallet(); loadWallet();
} }
Future<void> loadWallet() async { void loadWallet() async {
wallets = await WalletManager.listWallets(); wallets = await WalletManager.listWallets();
if (wallets.isEmpty && mounted) { if (wallets.isEmpty && mounted) {
unawaited( Navigator.of(context).pushReplacement(
Navigator.of(context) MaterialPageRoute(builder: (c) => const SetupView()));
.pushReplacement(platformRoute((c) => const SetupView())),
);
return; return;
} }
selectedWallet = wallets.first; selectedWallet = wallets.first;
selectedWallet!.recurEntries();
setState(() {}); setState(() {});
} }
@ -89,8 +77,7 @@ class _HomeViewState extends State<HomeView> {
// 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;
selectedWallet!.entries.clear(); selectedWallet!.entries.clear();
selectedWallet!.recurringEntries.clear(); var random = Random();
final random = Random();
for (var i = 0; i < 30; i++) { for (var i = 0; i < 30; i++) {
selectedWallet!.entries.add( selectedWallet!.entries.add(
WalletSingleEntry( WalletSingleEntry(
@ -113,42 +100,7 @@ class _HomeViewState extends State<HomeView> {
); );
} }
logger.d( logger.i(selectedWallet!.entries.length);
"Created ${selectedWallet!.entries.length} regular entries",
);
for (var i = 0; i < 3; i++) {
final type = random.nextInt(3);
selectedWallet!.recurringEntries.add(
RecurringWalletEntry(
data: EntryData(
name: "Recurring Entry #${i + 1}",
amount: random.nextInt(20000).toDouble(),
),
type: (random.nextInt(3) > 0)
? EntryType.expense
: EntryType.income,
date: DateTime(
2023,
random.nextInt(12) + 1,
random.nextInt(28) + 1,
),
category: selectedWallet!.categories[
random.nextInt(selectedWallet!.categories.length)],
id: selectedWallet!.nextId,
lastRunDate: DateTime.now().subtract(
Duration(
days: (type > 0) ? 3 : 3 * 31,
),
),
recurType: (type > 0) ? RecurType.day : RecurType.month,
),
);
}
logger.d(
"Created ${selectedWallet!.recurringEntries.length} recurring entries",
);
// save and reload // save and reload
WalletManager.saveWallet(selectedWallet!).then((value) { WalletManager.saveWallet(selectedWallet!).then((value) {
@ -164,23 +116,23 @@ class _HomeViewState extends State<HomeView> {
child: const Icon(Icons.edit), child: const Icon(Icons.edit),
label: AppLocalizations.of(context).addNew, label: AppLocalizations.of(context).addNew,
onTap: () async { onTap: () async {
final sw = await Navigator.of(context).push<Wallet>( var sw = await Navigator.of(context).push<Wallet>(
MaterialPageRoute( MaterialPageRoute(
builder: (c) => CreateSingleEntryView(w: selectedWallet!), builder: (c) => CreateEntryView(w: selectedWallet!),
), ),
); );
if (sw != null) { if (sw != null) {
selectedWallet = sw; selectedWallet = sw;
} }
setState(() {}); setState(() {});
}, }),
),
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 picker = ImagePicker(); final ImagePicker picker = ImagePicker();
final media = await picker.pickImage(source: ImageSource.camera); final XFile? media =
await picker.pickImage(source: ImageSource.camera);
logger.i(media?.name); logger.i(media?.name);
}, },
), ),
@ -209,7 +161,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 {
if (v == null || v == -1) { if (v == null || v == -1) {
@ -221,6 +173,7 @@ class _HomeViewState extends State<HomeView> {
), ),
); );
wallets = await WalletManager.listWallets(); wallets = await WalletManager.listWallets();
logger.i(wallets.length);
selectedWallet = wallets.last; selectedWallet = wallets.last;
setState(() {}); setState(() {});
return; return;
@ -233,29 +186,23 @@ class _HomeViewState extends State<HomeView> {
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) Navigator.of(context).push(
.push( MaterialPageRoute(
platformRoute( builder: (context) => const SettingsView(),
(context) => const SettingsView(),
), ),
) );
.then((value) async {
selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name);
});
} 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");
);
} }
}, },
), )
], ],
), ),
body: Center( body: Center(
@ -269,7 +216,7 @@ class _HomeViewState extends State<HomeView> {
width: 40, width: 40,
height: 40, height: 40,
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), )
], ],
) )
: (selectedWallet!.entries.isEmpty) : (selectedWallet!.entries.isEmpty)
@ -284,52 +231,50 @@ class _HomeViewState extends State<HomeView> {
), ),
Text( Text(
AppLocalizations.of(context).noEntriesSub, AppLocalizations.of(context).noEntriesSub,
), )
], ],
) )
: GroupedListView( : GroupedListView(
groupHeaderBuilder: (element) => Text( groupHeaderBuilder: (element) => Text(
DateFormat.yMMMM(locale).format(element.date), DateFormat.yMMMM(locale).format(element.date),
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary),
),
), ),
elements: selectedWallet!.entries, elements: selectedWallet!.entries,
itemComparator: (a, b) => b.date.compareTo(a.date), itemComparator: (a, b) => b.date.compareTo(a.date),
groupBy: (e) => DateFormat.yMMMM(locale).format(e.date), groupBy: (e) => DateFormat.yMMMM(locale).format(e.date),
groupComparator: (a, b) { groupComparator: (a, b) {
// TODO: better sorting algorithm lol // TODO: better sorting algorithm lol
final yearA = RegExp(r'\d+').firstMatch(a); var yearA = RegExp(r'\d+').firstMatch(a);
if (yearA == null) return 0; if (yearA == null) return 0;
final yearB = RegExp(r'\d+').firstMatch(b); var yearB = RegExp(r'\d+').firstMatch(b);
if (yearB == null) return 0; if (yearB == null) return 0;
final compareYears = int.parse(yearB.group(0)!) var compareYears = int.parse(yearA.group(0)!)
.compareTo(int.parse(yearA.group(0)!)); .compareTo(int.parse(yearB.group(0)!));
if (compareYears != 0) return compareYears; if (compareYears != 0) return compareYears;
final months = List<String>.generate( var months = List<String>.generate(
12, 12,
(index) => DateFormat.MMMM(locale).format( (index) => DateFormat.MMMM(locale).format(
DateTime(2023, index + 1), DateTime(2023, index + 1),
), ),
); );
final monthA = RegExp('[^0-9 ]+').firstMatch(a); var monthA = RegExp(r'[^0-9 ]+').firstMatch(a);
if (monthA == null) return 0; if (monthA == null) return 0;
final monthB = RegExp('[^0-9 ]+').firstMatch(b); var monthB = RegExp(r'[^0-9 ]+').firstMatch(b);
if (monthB == null) return 0; if (monthB == null) return 0;
return months.indexOf(monthB.group(0)!).compareTo( return months.indexOf(monthB.group(0)!).compareTo(
months.indexOf(monthA.group(0)!), months.indexOf(monthA.group(0)!),
); );
}, },
itemBuilder: (context, element) => Slidable( itemBuilder: (context, element) => Slidable(
endActionPane: ActionPane( endActionPane:
motion: const ScrollMotion(), ActionPane(motion: const ScrollMotion(), children: [
children: [
SlidableAction( SlidableAction(
onPressed: (c) { onPressed: (c) {
Navigator.of(context) Navigator.of(context)
.push<WalletSingleEntry>( .push<WalletSingleEntry>(
MaterialPageRoute( MaterialPageRoute(
builder: (c) => CreateSingleEntryView( builder: (c) => CreateEntryView(
w: selectedWallet!, w: selectedWallet!,
editEntry: element, editEntry: element,
), ),
@ -364,19 +309,15 @@ class _HomeViewState extends State<HomeView> {
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);
);
WalletManager.saveWallet( WalletManager.saveWallet(
selectedWallet!, selectedWallet!);
);
Navigator.of(cx).pop(); Navigator.of(cx).pop();
setState(() {}); setState(() {});
}, },
@ -392,66 +333,24 @@ class _HomeViewState extends State<HomeView> {
); );
}, },
), ),
], ]),
),
child: ListTile( child: ListTile(
leading: Container( leading: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: element.category.color, color: Theme.of(context).colorScheme.secondary),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Icon( child: Icon(
element.category.icon, element.category.icon,
color: color:
element.category.color.calculateTextColor(), Theme.of(context).colorScheme.onSecondary,
), ),
), ),
), ),
title: Text(element.data.name), title: Text(element.data.name),
subtitle: RichText( subtitle: Text(
text: TextSpan( "${element.data.amount} ${selectedWallet!.currency.symbol}"),
children: [
TextSpan(
text: NumberFormat.currency(
symbol: selectedWallet!.currency.symbol,
).format(element.data.amount),
style: TextStyle(
color: (element.type == EntryType.income)
? (MediaQuery.of(context)
.platformBrightness ==
Brightness.dark)
? Colors.green.shade300
: Colors.green.harmonizeWith(
Theme.of(context)
.colorScheme
.primary,
)
: (MediaQuery.of(context)
.platformBrightness ==
Brightness.dark)
? Colors.red.shade300
: Colors.red.harmonizeWith(
Theme.of(context)
.colorScheme
.primary,
),
),
),
TextSpan(
text:
" | ${DateFormat.MMMd(locale).format(element.date)}",
style: TextStyle(
color: Theme.of(context)
.colorScheme
.background
.calculateTextColor(),
),
),
],
),
),
), ),
), ),
), ),
@ -461,113 +360,91 @@ class _HomeViewState extends State<HomeView> {
} }
Future<void> startOcr(ImageSource imgSrc) async { Future<void> startOcr(ImageSource imgSrc) async {
final availableLanguages = await TessdataApi.getDownloadedData(); var availableLanguages = await TessdataApi.getDownloadedData();
if (availableLanguages.isEmpty) { if (availableLanguages.isEmpty) {
if (!mounted) return; if (!mounted) return;
await showDialog( ScaffoldMessenger.of(context).showSnackBar(
context: context, SnackBar(
builder: (c) => PlatformDialog( content: Text(AppLocalizations.of(context).missingOcr),
title: AppLocalizations.of(context).missingOcr, action: SnackBarAction(
actions: [ label: AppLocalizations.of(context).download,
PlatformButton(
text: AppLocalizations.of(context).download,
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
platformRoute( MaterialPageRoute(
(c) => const TessdataListView(), builder: (c) => const TessdataListView(),
), ),
); );
Navigator.of(c).pop();
}, },
), ),
PlatformButton(
text: AppLocalizations.of(context).ok,
onPressed: () {
Navigator.of(c).pop();
},
),
],
), ),
); );
return; return;
} }
if (!mounted) return; if (!mounted) return;
final selectedLanguages = var selectedLanguages = List<bool>.filled(availableLanguages.length, false);
List<bool>.filled(availableLanguages.length, false);
selectedLanguages[0] = true; selectedLanguages[0] = true;
await showDialog( showDialog(
context: context, context: context,
builder: (c) => StatefulBuilder( builder: (c) => StatefulBuilder(
builder: (ctx, setState) => PlatformDialog( builder: (ctx, setState) => PlatformDialog(
actions: [ actions: [
TextButton( TextButton(
onPressed: () async { onPressed: () async {
final picker = ImagePicker(); final ImagePicker picker = ImagePicker();
final media = await picker.pickImage(source: imgSrc); final XFile? media = await picker.pickImage(source: imgSrc);
if (media == null) { if (media == null) {
if (mounted) Navigator.of(context).pop(); if (mounted) Navigator.of(context).pop();
return; return;
} }
// get selected languages // get selected languages
final selected = availableLanguages var selected = availableLanguages
.where( .where((element) =>
(element) => selectedLanguages[ selectedLanguages[availableLanguages.indexOf(element)])
availableLanguages.indexOf(element)],
)
.join("+") .join("+")
.replaceAll(".traineddata", ""); .replaceAll(".traineddata", "");
logger.i(selected); logger.i(selected);
if (!mounted) return; if (!mounted) return;
unawaited(
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,
),
);
final string = await FlutterTesseractOcr.extractText(
media.path,
language: selected, language: selected,
args: { args: {
"psm": "4", "psm": "4",
"preserve_interword_spaces": "1", "preserve_interword_spaces": "1",
}, });
);
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
logger.i(string); logger.i(string);
if (!mounted) return; if (!mounted) return;
final lines = string.split("\n") var lines = string.split("\n")
..removeWhere((element) { ..removeWhere((element) {
element.trim(); element.trim();
return element.isEmpty; return element.isEmpty;
}); });
var price = 0.0; var price = 0.0;
final description = StringBuffer(); var description = "";
for (final line in lines) { for (var line in lines) {
// find numbered prices on each line // find numbered prices on each line
final regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+'); var regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+');
for (final match in regex.allMatches(line)) { for (var match in regex.allMatches(line)) {
price += double.tryParse(match.group(0).toString()) ?? 0; price += double.tryParse(match.group(0).toString()) ?? 0;
} }
description.write("${line.replaceAll(regex, "")}\n"); description += "${line.replaceAll(regex, "")}\n";
} }
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
// show edit // show edit
final newEntry = Navigator.of(context)
await Navigator.of(context).push<WalletSingleEntry>( .push<WalletSingleEntry>(
platformRoute<WalletSingleEntry>( platformRoute<WalletSingleEntry>(
(c) => CreateSingleEntryView( (c) => CreateEntryView(
w: selectedWallet!, w: selectedWallet!,
editEntry: WalletSingleEntry( editEntry: WalletSingleEntry(
data: EntryData( data: EntryData(
name: "", name: "", amount: price, description: description),
amount: price,
description: description.toString(),
),
type: EntryType.expense, type: EntryType.expense,
date: DateTime.now(), date: DateTime.now(),
category: selectedWallet!.categories.first, category: selectedWallet!.categories.first,
@ -575,12 +452,17 @@ class _HomeViewState extends State<HomeView> {
), ),
), ),
), ),
); )
.then(
(newEntry) {
// save entry if we didn't return empty
if (newEntry == null) return; if (newEntry == null) return;
selectedWallet!.entries.add(newEntry); selectedWallet!.entries.add(newEntry);
await WalletManager.saveWallet(selectedWallet!); WalletManager.saveWallet(selectedWallet!);
setState(() {}); setState(() {});
}, },
);
},
child: const Text("Ok"), child: const Text("Ok"),
), ),
TextButton( TextButton(
@ -613,10 +495,10 @@ class _HomeViewState extends State<HomeView> {
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
Text(availableLanguages[index].split(".").first), Text(availableLanguages[index].split(".").first)
], ],
), ),
), )
], ],
), ),
), ),
@ -625,12 +507,12 @@ class _HomeViewState extends State<HomeView> {
} }
Future<void> getLostData() async { Future<void> getLostData() async {
final picker = ImagePicker(); final ImagePicker picker = ImagePicker();
final response = await picker.retrieveLostData(); final LostDataResponse response = await picker.retrieveLostData();
if (response.isEmpty) { if (response.isEmpty) {
return; return;
} }
final files = response.files; final List<XFile>? files = response.files;
if (files != null) { if (files != null) {
logger.i("Found lost files"); logger.i("Found lost files");
_handleLostFiles(files); _handleLostFiles(files);

View file

@ -1,288 +0,0 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:prasule/api/recurring_entry.dart';
import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart';
import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/drawer.dart';
import 'package:prasule/util/text_color.dart';
import 'package:prasule/views/create_recur_entry.dart';
import 'package:prasule/views/settings/settings.dart';
import 'package:prasule/views/setup.dart';
/// Used to set up recurring entries
class RecurringEntriesView extends StatefulWidget {
/// Used to set up recurring entries
const RecurringEntriesView({super.key});
@override
State<RecurringEntriesView> createState() => _RecurringEntriesViewState();
}
class _RecurringEntriesViewState extends State<RecurringEntriesView> {
Wallet? selectedWallet;
List<Wallet> wallets = [];
late String locale;
@override
void didChangeDependencies() {
super.didChangeDependencies();
locale = Localizations.localeOf(context).languageCode;
initializeDateFormatting(Localizations.localeOf(context).languageCode);
}
@override
void initState() {
super.initState();
loadWallet();
}
Future<void> loadWallet() async {
wallets = await WalletManager.listWallets();
if (wallets.isEmpty && mounted) {
unawaited(
Navigator.of(context)
.pushReplacement(platformRoute((c) => const SetupView())),
);
return;
}
selectedWallet = wallets.first;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
drawer: makeDrawer(context, 3),
appBar: AppBar(
title: DropdownButton<int>(
value:
(selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!),
items: [
...wallets.map(
(e) => DropdownMenuItem(
value: wallets.indexOf(
e,
),
child: Text(e.name),
),
),
DropdownMenuItem(
value: -1,
child: Text(AppLocalizations.of(context).newWallet),
),
],
onChanged: (v) async {
if (v == null || v == -1) {
await Navigator.of(context).push(
platformRoute(
(c) => const SetupView(
newWallet: true,
),
),
);
wallets = await WalletManager.listWallets();
selectedWallet = wallets.last;
setState(() {});
return;
}
selectedWallet = wallets[v];
setState(() {});
},
),
actions: [
PopupMenuButton(
itemBuilder: (context) => [
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) {
Navigator.of(context)
.push(
platformRoute(
(context) => const SettingsView(),
),
)
.then((value) async {
selectedWallet =
await WalletManager.loadWallet(selectedWallet!.name);
});
} else if (value == AppLocalizations.of(context).about) {
showAboutDialog(
context: context,
applicationLegalese: AppLocalizations.of(context).license,
applicationName: "Prašule",
);
}
},
),
],
),
floatingActionButton: FloatingActionButton(
shape: const CircleBorder(),
child: const Icon(Icons.add),
onPressed: () {
Navigator.of(context).push(
platformRoute(
(p0) => CreateRecurringEntryView(
w: selectedWallet!,
locale: locale,
),
),
);
},
),
body: Center(
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: (selectedWallet == null)
? const Column(
children: [
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(),
),
],
)
: (selectedWallet!.recurringEntries.isEmpty)
? Column(
children: [
Text(
AppLocalizations.of(context).noEntries,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(
AppLocalizations.of(context).noEntriesSub,
),
],
)
: ListView.builder(
itemBuilder: (c, i) => Slidable(
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
onPressed: (c) {
Navigator.of(context)
.push<RecurringWalletEntry>(
MaterialPageRoute(
builder: (c) => CreateRecurringEntryView(
w: selectedWallet!,
locale: locale,
editEntry:
selectedWallet!.recurringEntries[i],
),
),
)
.then(
(editedEntry) {
if (editedEntry == null) return;
selectedWallet!.entries.remove(
selectedWallet!.recurringEntries[i],
);
selectedWallet!.entries.add(editedEntry);
WalletManager.saveWallet(selectedWallet!);
setState(() {});
},
);
},
backgroundColor:
Theme.of(context).colorScheme.secondary,
foregroundColor:
Theme.of(context).colorScheme.onSecondary,
icon: Icons.edit,
),
SlidableAction(
backgroundColor:
Theme.of(context).colorScheme.error,
foregroundColor:
Theme.of(context).colorScheme.onError,
icon: Icons.delete,
onPressed: (c) {
showDialog(
context: context,
builder: (cx) => PlatformDialog(
title:
AppLocalizations.of(context).sureDialog,
content: Text(
AppLocalizations.of(context).deleteSure,
),
actions: [
PlatformButton(
text: AppLocalizations.of(context).yes,
onPressed: () {
selectedWallet!.recurringEntries
.remove(
selectedWallet!.recurringEntries[i],
);
WalletManager.saveWallet(
selectedWallet!,
);
Navigator.of(cx).pop();
setState(() {});
},
),
PlatformButton(
text: AppLocalizations.of(context).no,
onPressed: () {
Navigator.of(cx).pop();
},
),
],
),
);
},
),
],
),
child: ListTile(
leading: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: selectedWallet!
.recurringEntries[i].category.color,
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
selectedWallet!
.recurringEntries[i].category.icon,
color: selectedWallet!
.recurringEntries[i].category.color
.calculateTextColor(),
),
),
),
title: Text(
selectedWallet!.recurringEntries[i].data.name,
),
subtitle: Text(
NumberFormat.currency(
symbol: selectedWallet!.currency.symbol,
).format(
selectedWallet!.recurringEntries[i].data.amount,
),
),
),
),
itemCount: selectedWallet!.recurringEntries.length,
),
),
),
);
}
}

View file

@ -1,292 +0,0 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'dart:async';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_iconpicker/flutter_iconpicker.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart';
import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart';
import 'package:prasule/pw/platformfield.dart';
import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/text_color.dart';
import 'package:prasule/views/settings/settings.dart';
import 'package:prasule/views/setup.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Allows adding, editing or removing [WalletCategory]s
class EditCategoriesView extends StatefulWidget {
/// Allows adding, editing or removing [WalletCategory]s
const EditCategoriesView({super.key});
@override
State<EditCategoriesView> createState() => _EditCategoriesViewState();
}
class _EditCategoriesViewState extends State<EditCategoriesView> {
Wallet? selectedWallet;
List<Wallet> wallets = [];
@override
void initState() {
super.initState();
loadWallet();
}
Future<void> loadWallet() async {
wallets = await WalletManager.listWallets();
if (wallets.isEmpty && mounted) {
unawaited(
Navigator.of(context)
.pushReplacement(platformRoute((c) => const SetupView())),
);
return;
}
selectedWallet = wallets.first;
logger.i(selectedWallet!.categories);
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: DropdownButton<int>(
value:
(selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!),
items: [
...wallets.map(
(e) => DropdownMenuItem(
value: wallets.indexOf(
e,
),
child: Text(e.name),
),
),
DropdownMenuItem(
value: -1,
child: Text(AppLocalizations.of(context).newWallet),
),
],
onChanged: (v) async {
if (v == null || v == -1) {
await Navigator.of(context).push(
platformRoute(
(c) => const SetupView(
newWallet: true,
),
),
);
wallets = await WalletManager.listWallets();
logger.i(wallets.length);
selectedWallet = wallets.last;
setState(() {});
return;
}
selectedWallet = wallets[v];
setState(() {});
},
),
actions: [
PopupMenuButton(
itemBuilder: (context) => [
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) {
Navigator.of(context).push(
platformRoute(
(context) => const SettingsView(),
),
);
} else if (value == AppLocalizations.of(context).about) {
showAboutDialog(
context: context,
applicationLegalese: AppLocalizations.of(context).license,
applicationName: "Prašule",
);
}
},
),
],
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: selectedWallet == null
? [const CircularProgressIndicator()]
: [
Text(
AppLocalizations.of(context).setupCategoriesEditHint,
textAlign: TextAlign.center,
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.64,
child: ListView.builder(
shrinkWrap: true,
itemBuilder: (context, i) => (i == 0)
? const SizedBox()
: ListTile(
leading: GestureDetector(
onTap: () async {
final icon =
await FlutterIconPicker.showIconPicker(
context,
);
if (icon != null) {
selectedWallet!.categories[i].icon = icon;
}
final materialEnabled =
(await SharedPreferences.getInstance())
.getBool("useMaterialYou") ??
false;
if (!mounted) return;
await showDialog(
context: context,
builder: (c) => PlatformDialog(
actions: [
PlatformButton(
text: AppLocalizations.of(context).done,
onPressed: () {
Navigator.of(c).pop();
},
),
],
title:
AppLocalizations.of(context).pickColor,
content: Column(
children: [
ColorPicker(
pickersEnabled: {
ColorPickerType.wheel: true,
ColorPickerType.primary: false,
ColorPickerType.custom: false,
ColorPickerType.bw: false,
ColorPickerType.accent:
materialEnabled,
},
color: selectedWallet!
.categories[i].color,
onColorChanged: (color) {
selectedWallet!
.categories[i].color = color;
setState(() {});
},
),
],
),
),
);
await WalletManager.saveWallet(selectedWallet!);
setState(() {});
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: selectedWallet!.categories[i].color,
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
selectedWallet!.categories[i].icon,
color: selectedWallet!.categories[i].color
.calculateTextColor(),
),
),
),
),
trailing: IconButton(
icon: const Icon(Icons.cancel),
onPressed: () async {
await selectedWallet!.removeCategory(
selectedWallet!.categories[i],
);
setState(() {});
},
),
title: GestureDetector(
onTap: () {
final controller = TextEditingController(
text: selectedWallet!.categories[i].name,
);
showDialog(
context: context,
builder: (c) => PlatformDialog(
actions: [
TextButton(
onPressed: () async {
if (controller.text.isEmpty) return;
selectedWallet!.categories[i].name =
controller.text;
await WalletManager.saveWallet(
selectedWallet!,
);
if (!mounted) return;
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).ok,
),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
AppLocalizations.of(context).cancel,
),
),
],
title: AppLocalizations.of(context)
.setupCategoriesEditingName,
content: SizedBox(
width: 400,
child:
PlatformField(controller: controller),
),
),
);
},
child: Text(
selectedWallet!.categories[i].name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
itemCount: selectedWallet!.categories.length,
),
),
IconButton(
onPressed: () async {
selectedWallet!.categories.add(
WalletCategory(
name: AppLocalizations.of(context)
.setupWalletNamePlaceholder,
id: selectedWallet!.nextCategoryId,
icon: IconData(
Icons.question_mark.codePoint,
fontFamily: 'MaterialIcons',
),
color: Colors.blueGrey.harmonizeWith(
Theme.of(context).colorScheme.primary,
),
),
);
await WalletManager.saveWallet(selectedWallet!);
setState(() {});
},
icon: const Icon(Icons.add),
),
],
),
);
}
}

View file

@ -1,14 +1,10 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformdialog.dart';
import 'package:settings_ui/settings_ui.dart'; import 'package:settings_ui/settings_ui.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Allows setting the type of graph for certain data
class GraphTypeSettingsView extends StatefulWidget { class GraphTypeSettingsView extends StatefulWidget {
/// Allows setting the type of graph for certain data
const GraphTypeSettingsView({super.key}); const GraphTypeSettingsView({super.key});
@override @override
@ -38,18 +34,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
applicationType: ApplicationType.both, applicationType: ApplicationType.both,
darkTheme: SettingsThemeData( darkTheme: SettingsThemeData(
settingsListBackground: Theme.of(context).colorScheme.background, settingsListBackground: Theme.of(context).colorScheme.background,
titleTextColor: Theme.of(context).colorScheme.primary, titleTextColor: Theme.of(context).colorScheme.primary),
),
sections: [ sections: [
SettingsSection( SettingsSection(
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
title: Text(AppLocalizations.of(context).yearly), title: Text(AppLocalizations.of(context).yearly),
value: Text( value: Text(_yearly == 1
_yearly == 1
? AppLocalizations.of(context).barChart ? AppLocalizations.of(context).barChart
: AppLocalizations.of(context).lineChart, : AppLocalizations.of(context).lineChart),
),
onPressed: (c) => showDialog( onPressed: (c) => showDialog(
context: c, context: c,
builder: (ctx) => PlatformDialog( builder: (ctx) => PlatformDialog(
@ -60,15 +53,13 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
width: MediaQuery.of(ctx).size.width, width: MediaQuery.of(ctx).size.width,
child: InkWell( child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(AppLocalizations.of(context).barChart,
AppLocalizations.of(context).barChart, textAlign: TextAlign.center),
textAlign: TextAlign.center,
),
), ),
onTap: () async { onTap: () async {
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
await s.setInt("yearlygraph", 1); s.setInt("yearlygraph", 1);
_yearly = 1; _yearly = 1;
if (!mounted) return; if (!mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
@ -80,15 +71,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
child: InkWell( child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
AppLocalizations.of(context).lineChart, AppLocalizations.of(context).lineChart,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
onTap: () async { onTap: () async {
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
await s.setInt("yearlygraph", 2); s.setInt("yearlygraph", 2);
_yearly = 2; _yearly = 2;
if (!mounted) return; if (!mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
@ -103,11 +94,9 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
), ),
SettingsTile.navigation( SettingsTile.navigation(
title: Text(AppLocalizations.of(context).monthly), title: Text(AppLocalizations.of(context).monthly),
value: Text( value: Text(_monthly == 1
_monthly == 1
? AppLocalizations.of(context).barChart ? AppLocalizations.of(context).barChart
: AppLocalizations.of(context).lineChart, : AppLocalizations.of(context).lineChart),
),
onPressed: (c) => showDialog( onPressed: (c) => showDialog(
context: c, context: c,
builder: (ctx) => PlatformDialog( builder: (ctx) => PlatformDialog(
@ -118,15 +107,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
width: MediaQuery.of(ctx).size.width, width: MediaQuery.of(ctx).size.width,
child: InkWell( child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
AppLocalizations.of(context).barChart, AppLocalizations.of(context).barChart,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
onTap: () async { onTap: () async {
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
await s.setInt("monthlygraph", 1); s.setInt("monthlygraph", 1);
_monthly = 1; _monthly = 1;
if (!mounted) return; if (!mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
@ -138,15 +127,14 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
width: MediaQuery.of(ctx).size.width, width: MediaQuery.of(ctx).size.width,
child: InkWell( child: InkWell(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Text( child: Text(
AppLocalizations.of(context).lineChart, AppLocalizations.of(context).lineChart,
textAlign: TextAlign.center, textAlign: TextAlign.center),
),
), ),
onTap: () async { onTap: () async {
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
await s.setInt("monthlygraph", 2); s.setInt("monthlygraph", 2);
_monthly = 2; _monthly = 2;
if (!mounted) return; if (!mounted) return;
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
@ -160,7 +148,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
), ),
), ),
], ],
), )
], ],
), ),
); );

View file

@ -1,18 +1,15 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
import 'package:prasule/pw/platformroute.dart'; import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/views/settings/edit_categories.dart';
import 'package:prasule/views/settings/graph_type.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:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Shows settings categories
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
/// Shows settings categories
const SettingsView({super.key}); const SettingsView({super.key});
@override @override
@ -39,25 +36,8 @@ class _SettingsViewState extends State<SettingsView> {
applicationType: ApplicationType.both, applicationType: ApplicationType.both,
darkTheme: SettingsThemeData( darkTheme: SettingsThemeData(
settingsListBackground: Theme.of(context).colorScheme.background, settingsListBackground: Theme.of(context).colorScheme.background,
titleTextColor: Theme.of(context).colorScheme.primary, titleTextColor: Theme.of(context).colorScheme.primary),
),
sections: [ sections: [
SettingsSection(
title: Text(AppLocalizations.of(context).wallet),
tiles: [
SettingsTile.navigation(
title: Text(AppLocalizations.of(context).editCategories),
description:
Text(AppLocalizations.of(context).editCategoriesDesc),
trailing: const Icon(Icons.keyboard_arrow_right),
onPressed: (context) => Navigator.of(context).push(
platformRoute(
(c) => const EditCategoriesView(),
),
),
),
],
),
SettingsSection( SettingsSection(
tiles: [ tiles: [
SettingsTile.navigation( SettingsTile.navigation(
@ -65,12 +45,9 @@ class _SettingsViewState extends State<SettingsView> {
description: description:
Text(AppLocalizations.of(context).downloadedOcrDesc), Text(AppLocalizations.of(context).downloadedOcrDesc),
trailing: const Icon(Icons.keyboard_arrow_right), trailing: const Icon(Icons.keyboard_arrow_right),
onPressed: (context) => Navigator.of(context).push( onPressed: (context) => Navigator.of(context)
platformRoute( .push(platformRoute((c) => const TessdataListView())),
(c) => const TessdataListView(), )
),
),
),
], ],
title: Text(AppLocalizations.of(context).ocr), title: Text(AppLocalizations.of(context).ocr),
), ),
@ -91,18 +68,16 @@ class _SettingsViewState extends State<SettingsView> {
SettingsTile.switchTile( SettingsTile.switchTile(
initialValue: _useMaterialYou, initialValue: _useMaterialYou,
onToggle: (v) async { onToggle: (v) async {
final s = await SharedPreferences.getInstance(); var s = await SharedPreferences.getInstance();
await s.setBool("useMaterialYou", v); s.setBool("useMaterialYou", v);
_useMaterialYou = v; _useMaterialYou = v;
setState(() {}); setState(() {});
}, },
title: Text(AppLocalizations.of(context).enableYou), title: Text(AppLocalizations.of(context).enableYou),
description: Text( description: Text(AppLocalizations.of(context).enableYouDesc),
AppLocalizations.of(context).enableYouDesc, )
),
),
], ],
), )
], ],
), ),
); );

View file

@ -1,19 +1,15 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
import 'package:prasule/main.dart'; import 'package:prasule/main.dart';
import 'package:prasule/network/tessdata.dart'; import 'package:prasule/network/tessdata.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformdialog.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Used to manage downloaded Tessdata for OCR
class TessdataListView extends StatefulWidget { class TessdataListView extends StatefulWidget {
/// Used to manage downloaded Tessdata for OCR
const TessdataListView({super.key}); const TessdataListView({super.key});
@override @override
@ -22,7 +18,7 @@ class TessdataListView extends StatefulWidget {
class _TessdataListViewState extends State<TessdataListView> { class _TessdataListViewState extends State<TessdataListView> {
final _tessdata = [ final _tessdata = [
{"eng": true}, {"eng": true}
]; ];
@override @override
void didChangeDependencies() { void didChangeDependencies() {
@ -53,22 +49,19 @@ class _TessdataListViewState extends State<TessdataListView> {
itemBuilder: (context, i) => ListTile( itemBuilder: (context, i) => ListTile(
title: Text(_tessdata[i].keys.first), title: Text(_tessdata[i].keys.first),
trailing: TextButton( trailing: TextButton(
child: Text( child: Text(_tessdata[i][_tessdata[i].keys.first]!
_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 {
final lang = _tessdata[i].keys.first; var lang = _tessdata[i].keys.first;
if (_tessdata[i][lang]!) { if (_tessdata[i][lang]!) {
// deleting data // deleting data
await showDialog( showDialog(
context: context, context: context,
builder: (context) => PlatformDialog( builder: (context) => PlatformDialog(
title: AppLocalizations.of(context).sureDialog, title: AppLocalizations.of(context).sureDialog,
content: Text( content: Text(AppLocalizations.of(context)
AppLocalizations.of(context).deleteOcr(lang), .deleteOcr(lang)),
),
actions: [ actions: [
PlatformButton( PlatformButton(
text: AppLocalizations.of(context).yes, text: AppLocalizations.of(context).yes,
@ -93,9 +86,8 @@ class _TessdataListViewState extends State<TessdataListView> {
// TODO: handle wifi errors // TODO: handle wifi errors
//* downloading traineddata //* downloading traineddata
final progressStream = StreamController<double>(); var progressStream = StreamController<double>();
unawaited(
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
@ -110,21 +102,16 @@ class _TessdataListViewState extends State<TessdataListView> {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Text("Error"); return const Text("Error");
} }
return Text( return Text(AppLocalizations.of(context)
AppLocalizations.of(context) .langDownloadProgress(snapshot.data!));
.langDownloadProgress(snapshot.data!),
);
}, },
stream: progressStream.stream, stream: progressStream.stream,
), ),
), ),
),
); );
await TessdataApi.downloadData( await TessdataApi.downloadData(lang, callback: (a, b) {
lang,
callback: (a, b) {
if (progressStream.isClosed) return; if (progressStream.isClosed) return;
final p = a / b * 1000; var p = a / b * 1000;
progressStream.add(p.roundToDouble() / 10); progressStream.add(p.roundToDouble() / 10);
if (p / 10 >= 100) { if (p / 10 >= 100) {
logger.i("Done"); logger.i("Done");
@ -134,8 +121,7 @@ class _TessdataListViewState extends State<TessdataListView> {
progressStream.close(); progressStream.close();
} }
setState(() {}); setState(() {});
}, });
);
}, },
), ),
), ),
@ -148,26 +134,25 @@ class _TessdataListViewState extends State<TessdataListView> {
/// Used to find which `.traineddata` is already downloaded and which not /// Used to find which `.traineddata` is already downloaded and which not
/// so we can show it to the user /// so we can show it to the user
Future<void> loadAllTessdata() async { void loadAllTessdata() async {
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath()); var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
final d = await TessdataApi.getAvailableData(); var d = await TessdataApi.getAvailableData();
final dataStatus = <Map<String, bool>>[]; var dataStatus = <Map<String, bool>>[];
for (final data in d) { for (var data in d) {
final e = <String, bool>{}; var e = <String, bool>{};
e[data] = false; e[data] = false;
dataStatus.add(e); dataStatus.add(e);
} }
final appDir = tessDir.listSync(); var appDir = tessDir.listSync();
for (final file in appDir) { for (var file in appDir) {
if (file is! File || if (file is! File ||
!file.path.endsWith("traineddata") || !file.path.endsWith("traineddata") ||
file.path.endsWith("eng.traineddata")) continue; file.path.endsWith("eng.traineddata")) continue;
logger.i(file.path); logger.i(file.path);
final filename = file.path.split("/").last; var filename = file.path.split("/").last;
dataStatus[dataStatus.indexWhere( dataStatus[dataStatus.indexWhere((element) =>
(element) => element.keys.first == filename.replaceAll(".traineddata", ""))]
element.keys.first == filename.replaceAll(".traineddata", ""), [filename.replaceAll(".traineddata", "")] = true;
)][filename.replaceAll(".traineddata", "")] = true;
} }
_tessdata.addAll(dataStatus); _tessdata.addAll(dataStatus);
setState(() {}); setState(() {});

View file

@ -1,28 +1,18 @@
// ignore_for_file: inference_failure_on_function_invocation
import 'package:currency_picker/currency_picker.dart'; import 'package:currency_picker/currency_picker.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_iconpicker/flutter_iconpicker.dart'; import 'package:flutter_iconpicker/flutter_iconpicker.dart';
import 'package:introduction_screen/introduction_screen.dart'; import 'package:introduction_screen/introduction_screen.dart';
import 'package:prasule/api/category.dart'; import 'package:prasule/api/category.dart';
import 'package:prasule/api/wallet.dart'; import 'package:prasule/api/wallet.dart';
import 'package:prasule/api/wallet_manager.dart'; import 'package:prasule/api/walletmanager.dart';
import 'package:prasule/pw/platformbutton.dart'; import 'package:prasule/pw/platformbutton.dart';
import 'package:prasule/pw/platformdialog.dart'; import 'package:prasule/pw/platformdialog.dart';
import 'package:prasule/pw/platformfield.dart'; import 'package:prasule/pw/platformfield.dart';
import 'package:prasule/pw/platformroute.dart';
import 'package:prasule/util/show_message.dart';
import 'package:prasule/util/text_color.dart';
import 'package:prasule/views/home.dart'; import 'package:prasule/views/home.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// View that shows on first-time setup
class SetupView extends StatefulWidget { class SetupView extends StatefulWidget {
/// View that shows on first-time setup
const SetupView({super.key, this.newWallet = false}); const SetupView({super.key, this.newWallet = false});
/// We are only creating a new wallet, no first-time setup /// We are only creating a new wallet, no first-time setup
@ -32,11 +22,10 @@ class SetupView extends StatefulWidget {
} }
class _SetupViewState extends State<SetupView> { class _SetupViewState extends State<SetupView> {
var _selectedCurrency = Currency.from( var _selectedCurrency = Currency.from(json: {
json: {
"code": "USD", "code": "USD",
"name": "United States Dollar", "name": "United States Dollar",
"symbol": r"$", "symbol": "\$",
"flag": "USD", "flag": "USD",
"decimal_digits": 2, "decimal_digits": 2,
"number": 840, "number": 840,
@ -45,58 +34,42 @@ class _SetupViewState extends State<SetupView> {
"decimal_separator": ".", "decimal_separator": ".",
"space_between_amount_and_symbol": false, "space_between_amount_and_symbol": false,
"symbol_on_left": true, "symbol_on_left": true,
}, });
); var categories = <WalletCategory>[];
List<WalletCategory> categories = <WalletCategory>[]; var name = "";
String name = ""; var balance = 0.0;
double balance = 0;
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
if (categories.isEmpty) { if (categories.isEmpty) {
categories = [ categories = [
WalletCategory(
name: AppLocalizations.of(context).noCategory,
id: 0,
icon: IconData(
Icons.payments.codePoint,
fontFamily: 'MaterialIcons',
),
color: Theme.of(context).colorScheme.secondary,
),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryHealth, name: AppLocalizations.of(context).categoryHealth,
type: EntryType.expense,
id: 1, id: 1,
icon: IconData( icon: IconData(Icons.medical_information.codePoint,
Icons.medical_information.codePoint, fontFamily: 'MaterialIcons'),
fontFamily: 'MaterialIcons',
),
color: Colors.red.shade700
.harmonizeWith(Theme.of(context).colorScheme.primary),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryCar, name: AppLocalizations.of(context).categoryCar,
type: EntryType.expense,
id: 2, id: 2,
icon: icon:
IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
color: Colors.purple
.harmonizeWith(Theme.of(context).colorScheme.primary),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryFood, name: AppLocalizations.of(context).categoryFood,
type: EntryType.expense,
id: 3, id: 3,
icon: icon:
IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'), IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
color: Colors.green.shade700
.harmonizeWith(Theme.of(context).colorScheme.primary),
), ),
WalletCategory( WalletCategory(
name: AppLocalizations.of(context).categoryTravel, name: AppLocalizations.of(context).categoryTravel,
type: EntryType.expense,
id: 4, id: 4,
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'), icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
color: Colors.orange.shade700
.harmonizeWith(Theme.of(context).colorScheme.primary),
), ),
]; ];
setState(() {}); setState(() {});
@ -114,29 +87,34 @@ class _SetupViewState extends State<SetupView> {
dotsDecorator: DotsDecorator( dotsDecorator: DotsDecorator(
activeColor: Theme.of(context).colorScheme.primary, activeColor: Theme.of(context).colorScheme.primary,
), ),
showNextButton: true,
showBackButton: true, showBackButton: 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) {
showMessage( ScaffoldMessenger.of(context)
AppLocalizations.of(context).errorEmptyName, .clearSnackBars(); // TODO: iOS replacement
context, ScaffoldMessenger.of(context).showSnackBar(SnackBar(
); content:
Text(AppLocalizations.of(context).errorEmptyName)));
return; return;
} }
final wallet = Wallet( var wallet = Wallet(
name: name, name: name,
currency: _selectedCurrency, currency: _selectedCurrency,
categories: categories, categories: categories);
);
WalletManager.saveWallet(wallet).then( WalletManager.saveWallet(wallet).then(
(value) { (value) {
if (!value) { if (!value) {
showMessage( ScaffoldMessenger.of(context).clearSnackBars();
AppLocalizations.of(context).walletExists, ScaffoldMessenger.of(context).showSnackBar(
context, SnackBar(
content:
Text(AppLocalizations.of(context).walletExists),
),
); );
return; return;
} }
@ -145,8 +123,8 @@ class _SetupViewState extends State<SetupView> {
return; return;
} }
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
platformRoute( MaterialPageRoute(
(c) => const HomeView(), builder: (c) => const HomeView(),
), ),
); );
}, },
@ -161,9 +139,7 @@ class _SetupViewState extends State<SetupView> {
child: Text( child: Text(
AppLocalizations.of(context).welcome, AppLocalizations.of(context).welcome,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@ -174,8 +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(
@ -183,8 +158,7 @@ class _SetupViewState extends State<SetupView> {
), ),
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context).welcomeInstruction, AppLocalizations.of(context).welcomeInstruction),
),
), ),
], ],
), ),
@ -198,9 +172,7 @@ class _SetupViewState extends State<SetupView> {
AppLocalizations.of(context).setupWalletNameCurrency, AppLocalizations.of(context).setupWalletNameCurrency,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
), ),
bodyWidget: Column( bodyWidget: Column(
@ -241,17 +213,14 @@ class _SetupViewState extends State<SetupView> {
labelText: labelText:
AppLocalizations.of(context).setupStartingBalance, AppLocalizations.of(context).setupStartingBalance,
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, decimal: true),
),
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow( FilteringTextInputFormatter.allow(
RegExp(r'\d+[\.,]{0,1}\d{0,}'), RegExp(r'\d+[\.,]{0,1}\d{0,}'),
), )
], ],
onChanged: (t) { onChanged: (t) {
final b = double.tryParse(t); balance = double.parse(t);
if (b == null) return;
balance = b;
}, },
), ),
), ),
@ -267,9 +236,7 @@ class _SetupViewState extends State<SetupView> {
AppLocalizations.of(context).setupCategoriesHeading, AppLocalizations.of(context).setupCategoriesHeading,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24, fontWeight: FontWeight.bold),
fontWeight: FontWeight.bold,
),
), ),
), ),
bodyWidget: Column( bodyWidget: Column(
@ -283,70 +250,26 @@ class _SetupViewState extends State<SetupView> {
height: MediaQuery.of(context).size.height * 0.64, height: MediaQuery.of(context).size.height * 0.64,
child: ListView.builder( child: ListView.builder(
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, i) => (i == 0) itemBuilder: (context, i) => ListTile(
? const SizedBox()
: ListTile(
leading: GestureDetector( leading: GestureDetector(
onTap: () async { onTap: () async {
final icon = var icon = await FlutterIconPicker.showIconPicker(
await FlutterIconPicker.showIconPicker( context);
context, if (icon == null) return;
); categories[i].icon = icon;
if (icon != null) categories[i].icon = icon;
final materialEnabled =
(await SharedPreferences.getInstance())
.getBool("useMaterialYou") ??
false;
if (!mounted) return;
await showDialog(
context: context,
builder: (c) => PlatformDialog(
actions: [
PlatformButton(
text: AppLocalizations.of(context)
.done,
onPressed: () {
Navigator.of(c).pop();
},
),
],
title: AppLocalizations.of(context)
.pickColor,
content: Column(
children: [
ColorPicker(
pickersEnabled: {
ColorPickerType.wheel: true,
ColorPickerType.primary: false,
ColorPickerType.custom: false,
ColorPickerType.bw: false,
ColorPickerType.accent:
materialEnabled,
},
color: categories[i].color,
onColorChanged: (color) {
categories[i].color = color;
setState(() {});
},
),
],
),
),
);
setState(() {}); setState(() {});
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
color: categories[i].color, color:
), Theme.of(context).colorScheme.secondary),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8.0),
child: Icon( child: Icon(
categories[i].icon, categories[i].icon,
color: categories[i] color:
.color Theme.of(context).colorScheme.onSecondary,
.calculateTextColor(),
), ),
), ),
), ),
@ -360,52 +283,43 @@ class _SetupViewState extends State<SetupView> {
), ),
title: GestureDetector( title: GestureDetector(
onTap: () { onTap: () {
final controller = TextEditingController( var controller = TextEditingController(
text: categories[i].name, text: categories[i].name);
);
showDialog( showDialog(
context: context, context: context,
builder: (c) => PlatformDialog( builder: (c) => PlatformDialog(
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
if (controller.text.isEmpty) { if (controller.text.isEmpty) return;
return; categories[i].name = controller.text;
}
categories[i].name =
controller.text;
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) AppLocalizations.of(context).cancel),
.cancel,
),
), ),
], ],
title: AppLocalizations.of(context) title: AppLocalizations.of(context)
.setupCategoriesEditingName, .setupCategoriesEditingName,
content: SizedBox( content: SizedBox(
width: 400, width: 400,
child: PlatformField( child:
controller: controller, PlatformField(controller: controller),
),
), ),
), ),
); );
}, },
child: Text( child: Text(
categories[i].name, categories[i].name,
style: const TextStyle( style:
fontWeight: FontWeight.bold, const TextStyle(fontWeight: FontWeight.bold),
),
), ),
), ),
), ),
@ -414,7 +328,7 @@ class _SetupViewState extends State<SetupView> {
), ),
IconButton( IconButton(
onPressed: () { onPressed: () {
var id = 0; var id = 1;
while (categories while (categories
.where((element) => element.id == id) .where((element) => element.id == id)
.isNotEmpty) { .isNotEmpty) {
@ -424,20 +338,16 @@ class _SetupViewState extends State<SetupView> {
WalletCategory( WalletCategory(
name: AppLocalizations.of(context) name: AppLocalizations.of(context)
.setupWalletNamePlaceholder, .setupWalletNamePlaceholder,
type: EntryType.expense,
id: id, id: id,
icon: IconData( icon: IconData(Icons.question_mark.codePoint,
Icons.question_mark.codePoint, fontFamily: 'MaterialIcons'),
fontFamily: 'MaterialIcons',
),
color: Colors.blueGrey.harmonizeWith(
Theme.of(context).colorScheme.primary,
),
), ),
); );
setState(() {}); setState(() {});
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
), )
], ],
), ),
), ),

View file

@ -21,10 +21,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.10" version: "3.4.9"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -317,26 +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"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
url: "https://pub.dev"
source: hosted
version: "3.3.0"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -466,14 +450,6 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
url: "https://pub.dev"
source: hosted
version: "8.2.4"
font_awesome_flutter: font_awesome_flutter:
dependency: transitive dependency: transitive
description: description:
@ -555,26 +531,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: image_picker name: image_picker
sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b" sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" 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: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec" sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.9+2" version: "0.8.9+1"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
description: description:
name: image_picker_for_web name: image_picker_for_web
sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 sha256: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.1"
image_picker_ios: image_picker_ios:
dependency: transitive dependency: transitive
description: description:
@ -603,10 +579,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
sha256: "0e827c156e3a90edd3bbe7f6de048b39247b16e58173b08a835b7eb00aba239e" sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.2" version: "2.9.1"
image_picker_windows: image_picker_windows:
dependency: transitive dependency: transitive
description: description:
@ -784,10 +760,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.2" version: "2.2.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
@ -840,10 +816,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.7"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
@ -1129,14 +1105,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
very_good_analysis:
dependency: "direct dev"
description:
name: very_good_analysis
sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1189,18 +1157,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.0" version: "5.1.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.3"
xml: xml:
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-alpha+3 version: 1.0.0-alpha+2
environment: environment:
sdk: '>=3.1.0-262.2.beta <4.0.0' sdk: '>=3.1.0-262.2.beta <4.0.0'
@ -13,35 +13,37 @@ environment:
# the latest version available on pub.dev. To see which dependencies have newer # the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`. # versions available, run `flutter pub outdated`.
dependencies: dependencies:
cupertino_icons: ^1.0.2
currency_picker: ^2.0.16
dio: ^5.3.0
dynamic_color: ^1.6.6
fl_chart: ^0.66.0
flex_color_picker: ^3.3.0
flutter: flutter:
sdk: flutter sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
path_provider: ^2.0.15
dio: ^5.3.0
logger: ^2.0.0
settings_ui: ^2.0.2
currency_picker: ^2.0.16
json_serializable: ^6.7.1
json_annotation: ^4.8.1
flutter_iconpicker: ^3.2.4 flutter_iconpicker: ^3.2.4
dynamic_color: ^1.6.6
introduction_screen: ^3.1.11
intl: any
grouped_list: ^5.1.2
flutter_speed_dial: ^7.0.0
image_picker: ^1.0.1
flutter_tesseract_ocr: ^0.4.23
flutter_slidable: ^3.0.0
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
flutter_slidable: ^3.0.0 fl_chart: ^0.65.0
flutter_speed_dial: ^7.0.0
flutter_tesseract_ocr: ^0.4.23
fluttertoast: ^8.2.4
grouped_list: ^5.1.2
image_picker: ^1.0.1
intl: any
introduction_screen: ^3.1.11
json_annotation: ^4.8.1
json_serializable: ^6.7.1
logger: ^2.0.0
path_provider: ^2.0.15
settings_ui: ^2.0.2
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
dev_dependencies: dev_dependencies:
build_runner: ^2.4.6 flutter_test:
flutter_launcher_icons: ^0.13.1 sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to # The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is # encourage good coding practices. The lint set provided by the package is
@ -49,12 +51,11 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
flutter_test: build_runner: ^2.4.6
sdk: flutter test: ^1.24.6
integration_test: integration_test:
sdk: flutter sdk: flutter
test: ^1.24.6 flutter_launcher_icons: ^0.13.1
very_good_analysis: ^5.1.0
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
@ -76,6 +77,7 @@ flutter_launcher_icons:
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
generate: true generate: true
@ -86,14 +88,18 @@ flutter:
assets: assets:
- assets/ - assets/
- assets/tessdata/ - assets/tessdata/
# To add assets to your application, add an assets section, like this: # To add assets to your application, add an assets section, like this:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see # For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages # https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here, # To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a # in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a # "family" key with the font family name, and a "fonts" key with a