Merge branch 'dev'
This commit is contained in:
commit
6675f91635
39 changed files with 2764 additions and 803 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,5 +1,14 @@
|
||||||
|
# 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
|
# 1.0.0-alpha
|
||||||
- First public release
|
- First public release
|
|
@ -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:flutter_lints/flutter.yaml
|
include: package:very_good_analysis/analysis_options.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,6 +23,8 @@ 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
|
||||||
|
|
|
@ -6,24 +6,33 @@ part 'category.g.dart';
|
||||||
|
|
||||||
/// Represents a category in a user's wallet
|
/// Represents a category in a user's wallet
|
||||||
class WalletCategory {
|
class WalletCategory {
|
||||||
final EntryType type;
|
/// Represents a category in a user's wallet
|
||||||
String name;
|
WalletCategory({
|
||||||
final int id;
|
required this.name,
|
||||||
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
|
|
||||||
IconData icon;
|
|
||||||
|
|
||||||
WalletCategory(
|
|
||||||
{required this.name,
|
|
||||||
required this.type,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.icon});
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
});
|
||||||
|
|
||||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
/// Connects generated fromJson method
|
||||||
/// factory.
|
|
||||||
factory WalletCategory.fromJson(Map<String, dynamic> json) =>
|
factory WalletCategory.fromJson(Map<String, dynamic> json) =>
|
||||||
_$WalletCategoryFromJson(json);
|
_$WalletCategoryFromJson(json);
|
||||||
|
|
||||||
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
|
/// User-defined name
|
||||||
|
String name;
|
||||||
|
|
||||||
|
/// Unique identificator of the category
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
/// Selected Icon for the category
|
||||||
|
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
|
||||||
|
IconData icon;
|
||||||
|
|
||||||
|
/// The color that will be displayed with entry
|
||||||
|
@JsonKey(fromJson: _colorFromJson, toJson: _colorToJson)
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
/// Connects generated toJson method
|
||||||
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
|
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -34,7 +43,18 @@ 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(data['codepoint'], fontFamily: data['family']);
|
|
||||||
|
|
||||||
enum EntryType { expense, income }
|
IconData _iconDataFromJson(Map<String, dynamic> data) =>
|
||||||
|
IconData(data['codepoint'] as int, fontFamily: data['family'] as String?);
|
||||||
|
|
||||||
|
int _colorToJson(Color color) => color.value;
|
||||||
|
Color _colorFromJson(int input) => Color(input);
|
||||||
|
|
||||||
|
/// Type of entry, either expense or income
|
||||||
|
enum EntryType {
|
||||||
|
/// Expense
|
||||||
|
expense,
|
||||||
|
|
||||||
|
/// Income
|
||||||
|
income
|
||||||
|
}
|
||||||
|
|
|
@ -9,20 +9,15 @@ 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',
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
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 {
|
||||||
String name;
|
/// Contains raw data
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
52
lib/api/recurring_entry.dart
Normal file
52
lib/api/recurring_entry.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
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
|
||||||
|
}
|
45
lib/api/recurring_entry.g.dart
Normal file
45
lib/api/recurring_entry.g.dart
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// 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',
|
||||||
|
};
|
|
@ -1,36 +1,59 @@
|
||||||
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/walletentry.dart';
|
import 'package:prasule/api/recurring_entry.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 {
|
||||||
final String name;
|
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
|
||||||
final List<WalletCategory> categories;
|
Wallet({
|
||||||
final List<WalletSingleEntry> entries;
|
required this.name,
|
||||||
double starterBalance;
|
|
||||||
@JsonKey(fromJson: _currencyFromJson)
|
|
||||||
final Currency currency;
|
|
||||||
|
|
||||||
Wallet(
|
|
||||||
{required this.name,
|
|
||||||
required this.currency,
|
required this.currency,
|
||||||
this.categories = const [],
|
this.categories = const [],
|
||||||
this.entries = const [],
|
this.entries = const [],
|
||||||
this.starterBalance = 0});
|
this.recurringEntries = const [],
|
||||||
|
this.starterBalance = 0,
|
||||||
|
});
|
||||||
|
|
||||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
/// Connects generated fromJson method
|
||||||
/// factory.
|
|
||||||
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
|
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
|
||||||
|
|
||||||
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
|
/// A list of all [RecurringWalletEntry]s
|
||||||
|
final List<RecurringWalletEntry> recurringEntries;
|
||||||
|
|
||||||
|
/// Name of the wallet
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// A list of available categories
|
||||||
|
final List<WalletCategory> categories;
|
||||||
|
|
||||||
|
/// List of saved entries
|
||||||
|
final List<WalletSingleEntry> entries;
|
||||||
|
|
||||||
|
/// The starting balance of the wallet
|
||||||
|
///
|
||||||
|
/// Used to calculate current balance
|
||||||
|
double starterBalance;
|
||||||
|
|
||||||
|
/// Selected currency
|
||||||
|
@JsonKey(fromJson: _currencyFromJson)
|
||||||
|
final Currency currency;
|
||||||
|
|
||||||
|
/// Connects generated 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) {
|
||||||
|
@ -39,13 +62,107 @@ 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": "\$",
|
"symbol": r"$",
|
||||||
"flag": "USD",
|
"flag": "USD",
|
||||||
"decimal_digits": 2,
|
"decimal_digits": 2,
|
||||||
"number": 840,
|
"number": 840,
|
||||||
|
|
|
@ -18,10 +18,16 @@ 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,
|
||||||
|
|
|
@ -1,30 +1,41 @@
|
||||||
import 'package:prasule/api/category.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/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 {
|
||||||
EntryType type;
|
/// This is an entry containing a single item
|
||||||
EntryData data;
|
WalletSingleEntry({
|
||||||
DateTime date;
|
required this.data,
|
||||||
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,
|
||||||
|
});
|
||||||
|
|
||||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
/// Connects generated fromJson method
|
||||||
/// factory.
|
|
||||||
factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
|
factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
_$WalletSingleEntryFromJson(json);
|
_$WalletSingleEntryFromJson(json);
|
||||||
|
|
||||||
/// Connect the generated [_$WalletEntryToJson] function to the `toJson` method.
|
/// Expense or income
|
||||||
|
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);
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'walletentry.dart';
|
part of 'wallet_entry.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// JsonSerializableGenerator
|
// JsonSerializableGenerator
|
|
@ -3,42 +3,52 @@ 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 {
|
||||||
var path =
|
final path =
|
||||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||||
if (!path.existsSync()) {
|
if (!path.existsSync()) {
|
||||||
path.createSync();
|
path.createSync();
|
||||||
}
|
}
|
||||||
var wallets = <Wallet>[];
|
final wallets = <Wallet>[];
|
||||||
for (var w in path.listSync().map((e) => e.path.split("/").last).toList()) {
|
for (final w
|
||||||
|
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 {
|
||||||
var path =
|
final path =
|
||||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||||
var wallet = File("${path.path}/$name");
|
final 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(jsonDecode(wallet.readAsStringSync()));
|
return Wallet.fromJson(
|
||||||
|
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 {
|
||||||
var path =
|
final path =
|
||||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||||
var wallet = File("${path.path}/${w.name}");
|
final wallet = File("${path.path}/${w.name}");
|
||||||
if (!path.existsSync()) {
|
if (!path.existsSync()) {
|
||||||
path.createSync();
|
path.createSync();
|
||||||
}
|
}
|
||||||
|
@ -47,10 +57,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 {
|
||||||
var path =
|
final path =
|
||||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||||
var wallet = File("${path.path}/${w.name}");
|
File("${path.path}/${w.name}").deleteSync();
|
||||||
wallet.deleteSync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -74,6 +74,18 @@
|
||||||
"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"
|
||||||
}
|
}
|
|
@ -154,5 +154,54 @@
|
||||||
"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'"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -3,25 +3,37 @@ 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();
|
||||||
var s = await SharedPreferences.getInstance();
|
final 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
|
||||||
|
@ -35,21 +47,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: (_materialYou)
|
colorScheme:
|
||||||
? dark ?? darkColorScheme
|
_materialYou ? dark ?? darkColorScheme : darkColorScheme,
|
||||||
: darkColorScheme),
|
),
|
||||||
home: const HomeView(),
|
home: const HomeView(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -57,11 +69,17 @@ class MyApp extends StatelessWidget {
|
||||||
: Theme(
|
: Theme(
|
||||||
data: ThemeData(
|
data: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: (MediaQuery.of(context).platformBrightness ==
|
colorScheme:
|
||||||
Brightness.dark)
|
(MediaQuery.of(context).platformBrightness == 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(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -4,40 +4,48 @@ 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 {
|
||||||
var res = await _client.get(
|
final res = await _client.get<List<Map<String, dynamic>>>(
|
||||||
"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}");
|
||||||
}
|
}
|
||||||
var data = res.data;
|
final data = res.data;
|
||||||
final dataFiles = <String>[];
|
final dataFiles = <String>[];
|
||||||
for (var file in data) {
|
for (final file in data ?? <Map<String, dynamic>>[]) {
|
||||||
if (!file["name"].endsWith(".traineddata")) continue;
|
if (!(file["name"] as String).endsWith(".traineddata")) continue;
|
||||||
dataFiles.add(file["name"].replaceAll(".traineddata", ""));
|
dataFiles.add((file["name"] as String).replaceAll(".traineddata", ""));
|
||||||
}
|
}
|
||||||
return dataFiles;
|
return dataFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes data from device
|
||||||
static Future<void> deleteData(String name) async {
|
static Future<void> deleteData(String name) async {
|
||||||
var dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
final dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||||
if (!dataDir.existsSync()) {
|
if (!dataDir.existsSync()) {
|
||||||
dataDir.createSync();
|
dataDir.createSync();
|
||||||
}
|
}
|
||||||
var dataFile = File("${dataDir.path}/$name.traineddata");
|
final 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 {
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||||
if (!tessDir.existsSync()) {
|
if (!tessDir.existsSync()) {
|
||||||
tessDir.createSync();
|
tessDir.createSync();
|
||||||
}
|
}
|
||||||
|
@ -48,25 +56,29 @@ class TessdataApi {
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> downloadData(String isoCode,
|
/// Downloads data from the repo to the device
|
||||||
{void Function(int, int)? callback}) async {
|
static Future<void> downloadData(
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
String isoCode, {
|
||||||
|
void Function(int, int)? callback,
|
||||||
|
}) async {
|
||||||
|
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||||
if (!tessDir.existsSync()) {
|
if (!tessDir.existsSync()) {
|
||||||
tessDir.createSync();
|
tessDir.createSync();
|
||||||
}
|
}
|
||||||
var file = File("${tessDir.path}/$isoCode.traineddata");
|
final file = File("${tessDir.path}/$isoCode.traineddata");
|
||||||
if (file.existsSync()) return; // TODO: maybe ask to redownload?
|
if (file.existsSync()) return; // TODO: maybe ask to redownload?
|
||||||
var res = await _client.get(
|
final res = await _client.get<List<int>>(
|
||||||
"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 {
|
||||||
var writefile = file.openSync(mode: FileMode.write);
|
file.openSync(mode: FileMode.write)
|
||||||
writefile.writeFromSync(res.data);
|
..writeFromSync(res.data!)
|
||||||
writefile.closeSync();
|
..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");
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
|
// 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(
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
|
// 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(
|
||||||
|
|
|
@ -1,9 +1,27 @@
|
||||||
|
// 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;
|
||||||
|
@ -16,20 +34,6 @@ 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(
|
||||||
|
@ -39,7 +43,8 @@ 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,
|
||||||
|
@ -56,7 +61,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled ?? true,
|
enabled: enabled ?? true,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
prefix: (labelText == null) ? null : Text(labelText!),
|
placeholder: labelText,
|
||||||
autocorrect: autocorrect,
|
autocorrect: autocorrect,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
inputFormatters: inputFormatters,
|
inputFormatters: inputFormatters,
|
||||||
|
|
|
@ -3,7 +3,10 @@ import 'dart:io';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
Route<T> platformRoute<T>(Widget Function(BuildContext) builder) =>
|
/// Creates a PageRoute based on [Platform]
|
||||||
|
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);
|
||||||
|
|
|
@ -5,6 +5,8 @@ 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
|
||||||
|
@ -16,7 +18,9 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// ignore_for_file: public_member_api_docs
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const lightColorScheme = ColorScheme(
|
const lightColorScheme = ColorScheme(
|
||||||
|
|
|
@ -3,6 +3,7 @@ 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(
|
||||||
|
@ -39,6 +40,22 @@ 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()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
8
lib/util/get_last_date.dart
Normal file
8
lib/util/get_last_date.dart
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/// Extension to get last day of the month
|
||||||
|
extension LastDay on DateTime {
|
||||||
|
/// Returns the last day of the month as [int]
|
||||||
|
int get lastDay {
|
||||||
|
final d = add(const Duration(days: 31));
|
||||||
|
return DateTime(d.year, d.month, 0).day;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,28 +2,67 @@ 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:intl/intl.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.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/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 {
|
||||||
const ExpensesLineChart(
|
/// Monthly/Yearly expense/income [LineChart]
|
||||||
{super.key,
|
const ExpensesLineChart({
|
||||||
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,
|
||||||
this.yearly = false});
|
super.key,
|
||||||
|
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 {
|
|
||||||
var list = List<double>.from(expenseData);
|
/// Expense data, but sorted
|
||||||
list.sort((a, b) => a.compareTo(b));
|
List<double> get expenseDataSorted =>
|
||||||
return list;
|
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
@ -52,46 +91,71 @@ class ExpensesLineChart extends StatelessWidget {
|
||||||
getTooltipItems: (spots) => List<LineTooltipItem>.generate(
|
getTooltipItems: (spots) => List<LineTooltipItem>.generate(
|
||||||
spots.length,
|
spots.length,
|
||||||
(index) => LineTooltipItem(
|
(index) => LineTooltipItem(
|
||||||
(spots[index].barIndex == 0)
|
// Changes what's rendered on the tooltip
|
||||||
|
// when clicked in the chart
|
||||||
|
(spots[index].barIndex == 0) // income chart
|
||||||
? (yearly
|
? (yearly
|
||||||
? AppLocalizations.of(context).incomeForMonth(
|
? AppLocalizations.of(context).incomeForMonth(
|
||||||
DateFormat.MMMM(locale).format(DateTime(
|
DateFormat.MMMM(locale).format(
|
||||||
date.year, spots[index].x.toInt() + 1, 1)),
|
DateTime(
|
||||||
|
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
|
: (yearly // expense chart
|
||||||
? AppLocalizations.of(context).expensesForMonth(
|
? AppLocalizations.of(context).expensesForMonth(
|
||||||
DateFormat.MMMM(locale).format(DateTime(
|
DateFormat.MMMM(locale).format(
|
||||||
date.year, spots[index].x.toInt() + 1, 1)),
|
DateTime(
|
||||||
|
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) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
|
maxX: yearly
|
||||||
|
? 12
|
||||||
|
: date.lastDay.toDouble() -
|
||||||
|
1, // remove 1 because we are indexing from 0
|
||||||
minY: 0,
|
minY: 0,
|
||||||
minX: 0,
|
minX: 0,
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
@ -102,11 +166,14 @@ class ExpensesLineChart extends StatelessWidget {
|
||||||
barWidth: 8,
|
barWidth: 8,
|
||||||
isStrokeCapRound: true,
|
isStrokeCapRound: true,
|
||||||
dotData: const FlDotData(show: false),
|
dotData: const FlDotData(show: false),
|
||||||
belowBarData: BarAreaData(show: false),
|
belowBarData: BarAreaData(),
|
||||||
color: Colors.green
|
color:
|
||||||
.harmonizeWith(Theme.of(context).colorScheme.secondary),
|
(MediaQuery.of(context).platformBrightness == Brightness.dark)
|
||||||
|
? Colors.green.shade300
|
||||||
|
: Colors.green
|
||||||
|
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||||
spots: List.generate(
|
spots: List.generate(
|
||||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
yearly ? 12 : date.lastDay,
|
||||||
(index) => FlSpot(index.toDouble(), incomeData[index]),
|
(index) => FlSpot(index.toDouble(), incomeData[index]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -116,21 +183,37 @@ class ExpensesLineChart extends StatelessWidget {
|
||||||
barWidth: 8,
|
barWidth: 8,
|
||||||
isStrokeCapRound: true,
|
isStrokeCapRound: true,
|
||||||
dotData: const FlDotData(show: false),
|
dotData: const FlDotData(show: false),
|
||||||
belowBarData: BarAreaData(show: false),
|
belowBarData: BarAreaData(),
|
||||||
color: Colors.red
|
color:
|
||||||
.harmonizeWith(Theme.of(context).colorScheme.secondary),
|
(MediaQuery.of(context).platformBrightness == Brightness.dark)
|
||||||
|
? Colors.red.shade300
|
||||||
|
: Colors.red
|
||||||
|
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||||
spots: List.generate(
|
spots: List.generate(
|
||||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
yearly
|
||||||
(index) => FlSpot(index.toDouble() + 1, expenseData[index]),
|
? 12
|
||||||
|
: 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(),
|
||||||
sideTitles: SideTitles(showTitles: false),
|
topTitles: const AxisTitles(),
|
||||||
|
leftTitles: AxisTitles(
|
||||||
|
sideTitles: SideTitles(
|
||||||
|
reservedSize: (NumberFormat.compact()
|
||||||
|
.format(expenseDataSorted.last)
|
||||||
|
.length >=
|
||||||
|
5 ||
|
||||||
|
NumberFormat.compact()
|
||||||
|
.format(incomeDataSorted.last)
|
||||||
|
.length >=
|
||||||
|
5)
|
||||||
|
? 50
|
||||||
|
: 25,
|
||||||
|
showTitles: true,
|
||||||
),
|
),
|
||||||
topTitles: const AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
),
|
||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
|
@ -140,13 +223,15 @@ 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, 1),
|
DateTime(date.year, value.toInt() + 1),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
text = (value.toInt() + 1).toString();
|
text = (value.toInt() + 1).toString();
|
||||||
}
|
}
|
||||||
return SideTitleWidget(
|
return SideTitleWidget(
|
||||||
axisSide: meta.axisSide, child: Text(text));
|
axisSide: meta.axisSide,
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -156,34 +241,52 @@ class ExpensesLineChart extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Renders expenses/income as a [BarChart]
|
||||||
class ExpensesBarChart extends StatelessWidget {
|
class ExpensesBarChart extends StatelessWidget {
|
||||||
const ExpensesBarChart(
|
/// Renders expenses/income as a [BarChart]
|
||||||
{super.key,
|
const ExpensesBarChart({
|
||||||
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,
|
||||||
final bool yearly;
|
super.key,
|
||||||
final DateTime date;
|
});
|
||||||
final String locale;
|
|
||||||
final List<double> expenseData;
|
|
||||||
List<double> get expenseDataSorted {
|
|
||||||
var list = List<double>.from(expenseData);
|
|
||||||
list.sort((a, b) => a.compareTo(b));
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// If the graph will be shown yearly
|
||||||
|
final bool yearly;
|
||||||
|
|
||||||
|
/// Selected date
|
||||||
|
///
|
||||||
|
/// Used to get either month or year
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
/// Current locale
|
||||||
|
///
|
||||||
|
/// Used mainly for formatting
|
||||||
|
final String locale;
|
||||||
|
|
||||||
|
/// The expense data used for the graph
|
||||||
|
final List<double> expenseData;
|
||||||
|
|
||||||
|
/// Wallet currency
|
||||||
|
///
|
||||||
|
/// Used to show currency symbol
|
||||||
final Currency currency;
|
final Currency currency;
|
||||||
|
|
||||||
final List<double> incomeData;
|
/// Expense data, but sorted
|
||||||
List<double> get incomeDataSorted {
|
List<double> get expenseDataSorted =>
|
||||||
var list = List<double>.from(incomeData);
|
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
||||||
list.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 {
|
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;
|
||||||
|
@ -201,26 +304,28 @@ 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, 1)),
|
DateTime(date.year, groupIndex + 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, 1)),
|
DateTime(date.year, groupIndex + 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),
|
||||||
)
|
)
|
||||||
|
@ -230,27 +335,23 @@ 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(),
|
||||||
sideTitles: SideTitles(showTitles: false),
|
topTitles: const AxisTitles(),
|
||||||
),
|
|
||||||
topTitles: const AxisTitles(
|
|
||||||
sideTitles: SideTitles(showTitles: false),
|
|
||||||
),
|
|
||||||
bottomTitles: AxisTitles(
|
bottomTitles: AxisTitles(
|
||||||
sideTitles: SideTitles(
|
sideTitles: SideTitles(
|
||||||
showTitles: true,
|
showTitles: true,
|
||||||
|
@ -259,13 +360,15 @@ 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, 1),
|
DateTime(date.year, value.toInt() + 1),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
text = (value.toInt() + 1).toString();
|
text = (value.toInt() + 1).toString();
|
||||||
}
|
}
|
||||||
return SideTitleWidget(
|
return SideTitleWidget(
|
||||||
axisSide: meta.axisSide, child: Text(text));
|
axisSide: meta.axisSide,
|
||||||
|
child: Text(text),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -273,7 +376,7 @@ class ExpensesBarChart extends StatelessWidget {
|
||||||
minY: 0,
|
minY: 0,
|
||||||
maxY: maxY,
|
maxY: maxY,
|
||||||
barGroups: List<BarChartGroupData>.generate(
|
barGroups: List<BarChartGroupData>.generate(
|
||||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
yearly ? 12 : date.lastDay - 1,
|
||||||
(index) => BarChartGroupData(
|
(index) => BarChartGroupData(
|
||||||
x: index,
|
x: index,
|
||||||
barRods: [
|
barRods: [
|
||||||
|
@ -281,13 +384,13 @@ class ExpensesBarChart extends StatelessWidget {
|
||||||
BarChartRodData(
|
BarChartRodData(
|
||||||
toY: incomeData[index],
|
toY: incomeData[index],
|
||||||
color: Colors.green
|
color: Colors.green
|
||||||
.harmonizeWith(Theme.of(context).colorScheme.secondary),
|
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
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.secondary),
|
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -295,3 +398,165 @@ 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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
15
lib/util/show_message.dart
Normal file
15
lib/util/show_message.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
11
lib/util/text_color.dart
Normal file
11
lib/util/text_color.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Used to add [calculateTextColor] to the [Color] class
|
||||||
|
extension TextColor on Color {
|
||||||
|
/// Returns if foreground should be white or dark on this [Color]
|
||||||
|
Color calculateTextColor() {
|
||||||
|
return ThemeData.estimateBrightnessForColor(this) == Brightness.light
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +1,33 @@
|
||||||
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/walletmanager.dart';
|
import 'package:prasule/api/wallet_entry.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:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:prasule/util/show_message.dart';
|
||||||
|
|
||||||
class CreateEntryView extends StatefulWidget {
|
/// Used when user wants to add new entry
|
||||||
|
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<CreateEntryView> createState() => _CreateEntryViewState();
|
State createState() => _CreateSingleEntryViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CreateEntryViewState extends State<CreateEntryView> {
|
class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
||||||
late WalletSingleEntry newEntry;
|
late WalletSingleEntry newEntry;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -31,7 +40,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
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(() {});
|
||||||
}
|
}
|
||||||
|
@ -68,12 +78,14 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
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);
|
||||||
|
@ -159,7 +171,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
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,
|
||||||
|
@ -178,13 +191,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
text: AppLocalizations.of(context).save,
|
text: AppLocalizations.of(context).save,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (newEntry.data.name.isEmpty) {
|
if (newEntry.data.name.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
showMessage(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppLocalizations.of(context).errorEmptyName, context);
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
AppLocalizations.of(context).errorEmptyName),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.editEntry != null) {
|
if (widget.editEntry != null) {
|
||||||
|
@ -196,7 +204,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
(value) => Navigator.of(context).pop(widget.w),
|
(value) => Navigator.of(context).pop(widget.w),
|
||||||
); // TODO loading circle?
|
); // TODO loading circle?
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
338
lib/views/create_recur_entry.dart
Normal file
338
lib/views/create_recur_entry.dart
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
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?
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,23 @@
|
||||||
|
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/walletmanager.dart';
|
import 'package:prasule/api/wallet_manager.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
|
||||||
|
@ -25,8 +29,8 @@ class _GraphViewState extends State<GraphView> {
|
||||||
Wallet? selectedWallet;
|
Wallet? selectedWallet;
|
||||||
List<Wallet> wallets = [];
|
List<Wallet> wallets = [];
|
||||||
String? locale;
|
String? locale;
|
||||||
var yearlyBtnSet = {"monthly"};
|
Set<String> yearlyBtnSet = {"monthly"};
|
||||||
var graphTypeSet = {"expense", "income"};
|
Set<String> graphTypeSet = {"expense", "income"};
|
||||||
bool get yearly => yearlyBtnSet.contains("yearly");
|
bool get yearly => yearlyBtnSet.contains("yearly");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -36,23 +40,25 @@ class _GraphViewState extends State<GraphView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<double> generateChartData(EntryType type) {
|
List<double> generateChartData(EntryType type) {
|
||||||
var data = List<double>.filled(
|
final d = _selectedDate.add(const Duration(days: 31));
|
||||||
(yearly)
|
final data = List<double>.filled(
|
||||||
? 12
|
yearly ? 12 : DateTime(d.year, d.month, 0).day,
|
||||||
: DateTime(_selectedDate.year, _selectedDate.month, 0).day,
|
0,
|
||||||
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++) {
|
||||||
var entriesForRange = selectedWallet!.entries.where((element) =>
|
final entriesForRange = selectedWallet!.entries.where(
|
||||||
|
(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 (var e in entriesForRange) {
|
for (final e in entriesForRange) {
|
||||||
sum += e.data.amount;
|
sum += e.data.amount;
|
||||||
}
|
}
|
||||||
data[i] = sum;
|
data[i] = sum;
|
||||||
|
@ -60,11 +66,13 @@ class _GraphViewState extends State<GraphView> {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadWallet() async {
|
Future<void> loadWallet() async {
|
||||||
wallets = await WalletManager.listWallets();
|
wallets = await WalletManager.listWallets();
|
||||||
if (wallets.isEmpty && mounted) {
|
if (wallets.isEmpty && mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
unawaited(
|
||||||
MaterialPageRoute(builder: (c) => const SetupView()));
|
Navigator.of(context)
|
||||||
|
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedWallet = wallets.first;
|
selectedWallet = wallets.first;
|
||||||
|
@ -85,6 +93,47 @@ 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:
|
||||||
|
@ -101,7 +150,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) {
|
||||||
|
@ -126,23 +175,32 @@ 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).push(
|
Navigator.of(context)
|
||||||
MaterialPageRoute(
|
.push(
|
||||||
builder: (context) => const SettingsView(),
|
platformRoute(
|
||||||
|
(context) => const SettingsView(),
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
|
.then((value) async {
|
||||||
|
selectedWallet =
|
||||||
|
await WalletManager.loadWallet(selectedWallet!.name);
|
||||||
|
final s = await SharedPreferences.getInstance();
|
||||||
|
chartType = s.getInt("monthlygraph") ?? 2;
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
} else if (value == AppLocalizations.of(context).about) {
|
} 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),
|
||||||
|
@ -193,8 +251,8 @@ class _GraphViewState extends State<GraphView> {
|
||||||
selected: yearlyBtnSet,
|
selected: yearlyBtnSet,
|
||||||
onSelectionChanged: (selection) async {
|
onSelectionChanged: (selection) async {
|
||||||
yearlyBtnSet = selection;
|
yearlyBtnSet = selection;
|
||||||
var s = await SharedPreferences.getInstance();
|
final 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(() {});
|
||||||
|
@ -204,61 +262,17 @@ class _GraphViewState extends State<GraphView> {
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context)
|
color:
|
||||||
.colorScheme
|
Theme.of(context).colorScheme.secondaryContainer,
|
||||||
.secondaryContainer),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
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: 300,
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.35,
|
||||||
child: (chartType == null)
|
child: (chartType == null)
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: (chartType == 1)
|
: (chartType == 1)
|
||||||
|
@ -270,35 +284,63 @@ 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,
|
||||||
|
)
|
||||||
: [],
|
: [],
|
||||||
)
|
)
|
||||||
: ExpensesLineChart(
|
: Padding(
|
||||||
currency: selectedWallet!.currency,
|
padding: const EdgeInsets.all(8),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
// 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';
|
||||||
|
@ -10,22 +16,25 @@ 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/walletentry.dart';
|
import 'package:prasule/api/recurring_entry.dart';
|
||||||
import 'package:prasule/api/wallet.dart';
|
import 'package:prasule/api/wallet.dart';
|
||||||
import 'package:prasule/api/walletmanager.dart';
|
import 'package:prasule/api/wallet_entry.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
|
||||||
|
@ -50,14 +59,17 @@ class _HomeViewState extends State<HomeView> {
|
||||||
loadWallet();
|
loadWallet();
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadWallet() async {
|
Future<void> loadWallet() async {
|
||||||
wallets = await WalletManager.listWallets();
|
wallets = await WalletManager.listWallets();
|
||||||
if (wallets.isEmpty && mounted) {
|
if (wallets.isEmpty && mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
unawaited(
|
||||||
MaterialPageRoute(builder: (c) => const SetupView()));
|
Navigator.of(context)
|
||||||
|
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
selectedWallet = wallets.first;
|
selectedWallet = wallets.first;
|
||||||
|
selectedWallet!.recurEntries();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +89,8 @@ 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();
|
||||||
var random = Random();
|
selectedWallet!.recurringEntries.clear();
|
||||||
|
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(
|
||||||
|
@ -100,7 +113,42 @@ class _HomeViewState extends State<HomeView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.i(selectedWallet!.entries.length);
|
logger.d(
|
||||||
|
"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) {
|
||||||
|
@ -116,23 +164,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 {
|
||||||
var sw = await Navigator.of(context).push<Wallet>(
|
final sw = await Navigator.of(context).push<Wallet>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (c) => CreateEntryView(w: selectedWallet!),
|
builder: (c) => CreateSingleEntryView(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 ImagePicker picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final XFile? media =
|
final media = await picker.pickImage(source: ImageSource.camera);
|
||||||
await picker.pickImage(source: ImageSource.camera);
|
|
||||||
logger.i(media?.name);
|
logger.i(media?.name);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -161,7 +209,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) {
|
||||||
|
@ -173,7 +221,6 @@ 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;
|
||||||
|
@ -186,23 +233,29 @@ 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).push(
|
Navigator.of(context)
|
||||||
MaterialPageRoute(
|
.push(
|
||||||
builder: (context) => const SettingsView(),
|
platformRoute(
|
||||||
|
(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(
|
||||||
|
@ -216,7 +269,7 @@ class _HomeViewState extends State<HomeView> {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
: (selectedWallet!.entries.isEmpty)
|
: (selectedWallet!.entries.isEmpty)
|
||||||
|
@ -231,50 +284,52 @@ 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
|
||||||
var yearA = RegExp(r'\d+').firstMatch(a);
|
final yearA = RegExp(r'\d+').firstMatch(a);
|
||||||
if (yearA == null) return 0;
|
if (yearA == null) return 0;
|
||||||
var yearB = RegExp(r'\d+').firstMatch(b);
|
final yearB = RegExp(r'\d+').firstMatch(b);
|
||||||
if (yearB == null) return 0;
|
if (yearB == null) return 0;
|
||||||
var compareYears = int.parse(yearA.group(0)!)
|
final compareYears = int.parse(yearB.group(0)!)
|
||||||
.compareTo(int.parse(yearB.group(0)!));
|
.compareTo(int.parse(yearA.group(0)!));
|
||||||
if (compareYears != 0) return compareYears;
|
if (compareYears != 0) return compareYears;
|
||||||
var months = List<String>.generate(
|
final months = List<String>.generate(
|
||||||
12,
|
12,
|
||||||
(index) => DateFormat.MMMM(locale).format(
|
(index) => DateFormat.MMMM(locale).format(
|
||||||
DateTime(2023, index + 1),
|
DateTime(2023, index + 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
var monthA = RegExp(r'[^0-9 ]+').firstMatch(a);
|
final monthA = RegExp('[^0-9 ]+').firstMatch(a);
|
||||||
if (monthA == null) return 0;
|
if (monthA == null) return 0;
|
||||||
var monthB = RegExp(r'[^0-9 ]+').firstMatch(b);
|
final monthB = RegExp('[^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:
|
endActionPane: ActionPane(
|
||||||
ActionPane(motion: const ScrollMotion(), children: [
|
motion: const ScrollMotion(),
|
||||||
|
children: [
|
||||||
SlidableAction(
|
SlidableAction(
|
||||||
onPressed: (c) {
|
onPressed: (c) {
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.push<WalletSingleEntry>(
|
.push<WalletSingleEntry>(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (c) => CreateEntryView(
|
builder: (c) => CreateSingleEntryView(
|
||||||
w: selectedWallet!,
|
w: selectedWallet!,
|
||||||
editEntry: element,
|
editEntry: element,
|
||||||
),
|
),
|
||||||
|
@ -309,15 +364,19 @@ 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(() {});
|
||||||
},
|
},
|
||||||
|
@ -333,24 +392,66 @@ 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: Theme.of(context).colorScheme.secondary),
|
color: element.category.color,
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
element.category.icon,
|
element.category.icon,
|
||||||
color:
|
color:
|
||||||
Theme.of(context).colorScheme.onSecondary,
|
element.category.color.calculateTextColor(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(element.data.name),
|
title: Text(element.data.name),
|
||||||
subtitle: Text(
|
subtitle: RichText(
|
||||||
"${element.data.amount} ${selectedWallet!.currency.symbol}"),
|
text: TextSpan(
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -360,91 +461,113 @@ class _HomeViewState extends State<HomeView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startOcr(ImageSource imgSrc) async {
|
Future<void> startOcr(ImageSource imgSrc) async {
|
||||||
var availableLanguages = await TessdataApi.getDownloadedData();
|
final availableLanguages = await TessdataApi.getDownloadedData();
|
||||||
if (availableLanguages.isEmpty) {
|
if (availableLanguages.isEmpty) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
await showDialog(
|
||||||
SnackBar(
|
context: context,
|
||||||
content: Text(AppLocalizations.of(context).missingOcr),
|
builder: (c) => PlatformDialog(
|
||||||
action: SnackBarAction(
|
title: AppLocalizations.of(context).missingOcr,
|
||||||
label: AppLocalizations.of(context).download,
|
actions: [
|
||||||
|
PlatformButton(
|
||||||
|
text: AppLocalizations.of(context).download,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).push(
|
Navigator.of(context).push(
|
||||||
MaterialPageRoute(
|
platformRoute(
|
||||||
builder: (c) => const TessdataListView(),
|
(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;
|
||||||
var selectedLanguages = List<bool>.filled(availableLanguages.length, false);
|
final selectedLanguages =
|
||||||
|
List<bool>.filled(availableLanguages.length, false);
|
||||||
selectedLanguages[0] = true;
|
selectedLanguages[0] = true;
|
||||||
|
|
||||||
showDialog(
|
await 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 ImagePicker picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final XFile? media = await picker.pickImage(source: imgSrc);
|
final 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
|
||||||
var selected = availableLanguages
|
final selected = availableLanguages
|
||||||
.where((element) =>
|
.where(
|
||||||
selectedLanguages[availableLanguages.indexOf(element)])
|
(element) => selectedLanguages[
|
||||||
|
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);
|
),
|
||||||
var string = await FlutterTesseractOcr.extractText(media.path,
|
barrierDismissible: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
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;
|
||||||
var lines = string.split("\n")
|
final 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;
|
||||||
var description = "";
|
final description = StringBuffer();
|
||||||
for (var line in lines) {
|
for (final line in lines) {
|
||||||
// find numbered prices on each line
|
// find numbered prices on each line
|
||||||
var regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+');
|
final regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+');
|
||||||
for (var match in regex.allMatches(line)) {
|
for (final match in regex.allMatches(line)) {
|
||||||
price += double.tryParse(match.group(0).toString()) ?? 0;
|
price += double.tryParse(match.group(0).toString()) ?? 0;
|
||||||
}
|
}
|
||||||
description += "${line.replaceAll(regex, "")}\n";
|
description.write("${line.replaceAll(regex, "")}\n");
|
||||||
}
|
}
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
// show edit
|
// show edit
|
||||||
Navigator.of(context)
|
final newEntry =
|
||||||
.push<WalletSingleEntry>(
|
await Navigator.of(context).push<WalletSingleEntry>(
|
||||||
platformRoute<WalletSingleEntry>(
|
platformRoute<WalletSingleEntry>(
|
||||||
(c) => CreateEntryView(
|
(c) => CreateSingleEntryView(
|
||||||
w: selectedWallet!,
|
w: selectedWallet!,
|
||||||
editEntry: WalletSingleEntry(
|
editEntry: WalletSingleEntry(
|
||||||
data: EntryData(
|
data: EntryData(
|
||||||
name: "", amount: price, description: description),
|
name: "",
|
||||||
|
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,
|
||||||
|
@ -452,17 +575,12 @@ 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);
|
||||||
WalletManager.saveWallet(selectedWallet!);
|
await WalletManager.saveWallet(selectedWallet!);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text("Ok"),
|
child: const Text("Ok"),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
|
@ -495,10 +613,10 @@ class _HomeViewState extends State<HomeView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
width: 10,
|
width: 10,
|
||||||
),
|
),
|
||||||
Text(availableLanguages[index].split(".").first)
|
Text(availableLanguages[index].split(".").first),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -507,12 +625,12 @@ class _HomeViewState extends State<HomeView> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> getLostData() async {
|
Future<void> getLostData() async {
|
||||||
final ImagePicker picker = ImagePicker();
|
final picker = ImagePicker();
|
||||||
final LostDataResponse response = await picker.retrieveLostData();
|
final response = await picker.retrieveLostData();
|
||||||
if (response.isEmpty) {
|
if (response.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final List<XFile>? files = response.files;
|
final files = response.files;
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
logger.i("Found lost files");
|
logger.i("Found lost files");
|
||||||
_handleLostFiles(files);
|
_handleLostFiles(files);
|
||||||
|
|
288
lib/views/recurring_view.dart
Normal file
288
lib/views/recurring_view.dart
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
292
lib/views/settings/edit_categories.dart
Normal file
292
lib/views/settings/edit_categories.dart
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
// 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,14 @@
|
||||||
|
// 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
|
||||||
|
@ -34,15 +38,18 @@ 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(_yearly == 1
|
value: Text(
|
||||||
|
_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(
|
||||||
|
@ -53,13 +60,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.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(AppLocalizations.of(context).barChart,
|
child: Text(
|
||||||
textAlign: TextAlign.center),
|
AppLocalizations.of(context).barChart,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var s = await SharedPreferences.getInstance();
|
final s = await SharedPreferences.getInstance();
|
||||||
s.setInt("yearlygraph", 1);
|
await s.setInt("yearlygraph", 1);
|
||||||
_yearly = 1;
|
_yearly = 1;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
|
@ -71,15 +80,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.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).lineChart,
|
AppLocalizations.of(context).lineChart,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var s = await SharedPreferences.getInstance();
|
final s = await SharedPreferences.getInstance();
|
||||||
s.setInt("yearlygraph", 2);
|
await s.setInt("yearlygraph", 2);
|
||||||
_yearly = 2;
|
_yearly = 2;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
|
@ -94,9 +103,11 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
||||||
),
|
),
|
||||||
SettingsTile.navigation(
|
SettingsTile.navigation(
|
||||||
title: Text(AppLocalizations.of(context).monthly),
|
title: Text(AppLocalizations.of(context).monthly),
|
||||||
value: Text(_monthly == 1
|
value: Text(
|
||||||
|
_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(
|
||||||
|
@ -107,15 +118,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.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).barChart,
|
AppLocalizations.of(context).barChart,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var s = await SharedPreferences.getInstance();
|
final s = await SharedPreferences.getInstance();
|
||||||
s.setInt("monthlygraph", 1);
|
await s.setInt("monthlygraph", 1);
|
||||||
_monthly = 1;
|
_monthly = 1;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
|
@ -127,14 +138,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.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).lineChart,
|
AppLocalizations.of(context).lineChart,
|
||||||
textAlign: TextAlign.center),
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var s = await SharedPreferences.getInstance();
|
final s = await SharedPreferences.getInstance();
|
||||||
s.setInt("monthlygraph", 2);
|
await s.setInt("monthlygraph", 2);
|
||||||
_monthly = 2;
|
_monthly = 2;
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
|
@ -148,7 +160,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
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
|
||||||
|
@ -36,8 +39,25 @@ 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(
|
||||||
|
@ -45,9 +65,12 @@ 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)
|
onPressed: (context) => Navigator.of(context).push(
|
||||||
.push(platformRoute((c) => const TessdataListView())),
|
platformRoute(
|
||||||
)
|
(c) => const TessdataListView(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
title: Text(AppLocalizations.of(context).ocr),
|
title: Text(AppLocalizations.of(context).ocr),
|
||||||
),
|
),
|
||||||
|
@ -68,16 +91,18 @@ class _SettingsViewState extends State<SettingsView> {
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
initialValue: _useMaterialYou,
|
initialValue: _useMaterialYou,
|
||||||
onToggle: (v) async {
|
onToggle: (v) async {
|
||||||
var s = await SharedPreferences.getInstance();
|
final s = await SharedPreferences.getInstance();
|
||||||
s.setBool("useMaterialYou", v);
|
await s.setBool("useMaterialYou", v);
|
||||||
_useMaterialYou = v;
|
_useMaterialYou = v;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
title: Text(AppLocalizations.of(context).enableYou),
|
title: Text(AppLocalizations.of(context).enableYou),
|
||||||
description: Text(AppLocalizations.of(context).enableYouDesc),
|
description: Text(
|
||||||
)
|
AppLocalizations.of(context).enableYouDesc,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
|
// 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
|
||||||
|
@ -18,7 +22,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() {
|
||||||
|
@ -49,19 +53,22 @@ 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(_tessdata[i][_tessdata[i].keys.first]!
|
child: Text(
|
||||||
|
_tessdata[i][_tessdata[i].keys.first]!
|
||||||
? AppLocalizations.of(context).downloaded
|
? AppLocalizations.of(context).downloaded
|
||||||
: AppLocalizations.of(context).download),
|
: AppLocalizations.of(context).download,
|
||||||
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var lang = _tessdata[i].keys.first;
|
final lang = _tessdata[i].keys.first;
|
||||||
if (_tessdata[i][lang]!) {
|
if (_tessdata[i][lang]!) {
|
||||||
// deleting data
|
// deleting data
|
||||||
showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => PlatformDialog(
|
builder: (context) => PlatformDialog(
|
||||||
title: AppLocalizations.of(context).sureDialog,
|
title: AppLocalizations.of(context).sureDialog,
|
||||||
content: Text(AppLocalizations.of(context)
|
content: Text(
|
||||||
.deleteOcr(lang)),
|
AppLocalizations.of(context).deleteOcr(lang),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
PlatformButton(
|
PlatformButton(
|
||||||
text: AppLocalizations.of(context).yes,
|
text: AppLocalizations.of(context).yes,
|
||||||
|
@ -86,8 +93,9 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
|
|
||||||
// TODO: handle wifi errors
|
// TODO: handle wifi errors
|
||||||
//* downloading traineddata
|
//* downloading traineddata
|
||||||
var progressStream = StreamController<double>();
|
final progressStream = StreamController<double>();
|
||||||
|
|
||||||
|
unawaited(
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (c) => PlatformDialog(
|
builder: (c) => PlatformDialog(
|
||||||
|
@ -102,16 +110,21 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return const Text("Error");
|
return const Text("Error");
|
||||||
}
|
}
|
||||||
return Text(AppLocalizations.of(context)
|
return Text(
|
||||||
.langDownloadProgress(snapshot.data!));
|
AppLocalizations.of(context)
|
||||||
|
.langDownloadProgress(snapshot.data!),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
stream: progressStream.stream,
|
stream: progressStream.stream,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await TessdataApi.downloadData(lang, callback: (a, b) {
|
await TessdataApi.downloadData(
|
||||||
|
lang,
|
||||||
|
callback: (a, b) {
|
||||||
if (progressStream.isClosed) return;
|
if (progressStream.isClosed) return;
|
||||||
var p = a / b * 1000;
|
final 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");
|
||||||
|
@ -121,7 +134,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
progressStream.close();
|
progressStream.close();
|
||||||
}
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -134,25 +148,26 @@ 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
|
||||||
void loadAllTessdata() async {
|
Future<void> loadAllTessdata() async {
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||||
var d = await TessdataApi.getAvailableData();
|
final d = await TessdataApi.getAvailableData();
|
||||||
var dataStatus = <Map<String, bool>>[];
|
final dataStatus = <Map<String, bool>>[];
|
||||||
for (var data in d) {
|
for (final data in d) {
|
||||||
var e = <String, bool>{};
|
final e = <String, bool>{};
|
||||||
e[data] = false;
|
e[data] = false;
|
||||||
dataStatus.add(e);
|
dataStatus.add(e);
|
||||||
}
|
}
|
||||||
var appDir = tessDir.listSync();
|
final appDir = tessDir.listSync();
|
||||||
for (var file in appDir) {
|
for (final 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);
|
||||||
var filename = file.path.split("/").last;
|
final filename = file.path.split("/").last;
|
||||||
dataStatus[dataStatus.indexWhere((element) =>
|
dataStatus[dataStatus.indexWhere(
|
||||||
element.keys.first == filename.replaceAll(".traineddata", ""))]
|
(element) =>
|
||||||
[filename.replaceAll(".traineddata", "")] = true;
|
element.keys.first == filename.replaceAll(".traineddata", ""),
|
||||||
|
)][filename.replaceAll(".traineddata", "")] = true;
|
||||||
}
|
}
|
||||||
_tessdata.addAll(dataStatus);
|
_tessdata.addAll(dataStatus);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
|
// 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/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/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:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:shared_preferences/shared_preferences.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
|
||||||
|
@ -22,10 +32,11 @@ class SetupView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SetupViewState extends State<SetupView> {
|
class _SetupViewState extends State<SetupView> {
|
||||||
var _selectedCurrency = Currency.from(json: {
|
var _selectedCurrency = Currency.from(
|
||||||
|
json: {
|
||||||
"code": "USD",
|
"code": "USD",
|
||||||
"name": "United States Dollar",
|
"name": "United States Dollar",
|
||||||
"symbol": "\$",
|
"symbol": r"$",
|
||||||
"flag": "USD",
|
"flag": "USD",
|
||||||
"decimal_digits": 2,
|
"decimal_digits": 2,
|
||||||
"number": 840,
|
"number": 840,
|
||||||
|
@ -34,42 +45,58 @@ 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>[];
|
);
|
||||||
var name = "";
|
List<WalletCategory> categories = <WalletCategory>[];
|
||||||
var balance = 0.0;
|
String name = "";
|
||||||
|
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(Icons.medical_information.codePoint,
|
icon: IconData(
|
||||||
fontFamily: 'MaterialIcons'),
|
Icons.medical_information.codePoint,
|
||||||
|
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(() {});
|
||||||
|
@ -87,34 +114,29 @@ 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) {
|
||||||
ScaffoldMessenger.of(context)
|
showMessage(
|
||||||
.clearSnackBars(); // TODO: iOS replacement
|
AppLocalizations.of(context).errorEmptyName,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
context,
|
||||||
content:
|
);
|
||||||
Text(AppLocalizations.of(context).errorEmptyName)));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var wallet = Wallet(
|
final 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) {
|
||||||
ScaffoldMessenger.of(context).clearSnackBars();
|
showMessage(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
AppLocalizations.of(context).walletExists,
|
||||||
SnackBar(
|
context,
|
||||||
content:
|
|
||||||
Text(AppLocalizations.of(context).walletExists),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -123,8 +145,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
platformRoute(
|
||||||
builder: (c) => const HomeView(),
|
(c) => const HomeView(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -139,7 +161,9 @@ 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, fontWeight: FontWeight.bold),
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -150,7 +174,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
if (!widget.newWallet)
|
if (!widget.newWallet)
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).welcomeAboutPrasule),
|
AppLocalizations.of(context).welcomeAboutPrasule,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
if (!widget.newWallet)
|
if (!widget.newWallet)
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -158,7 +183,8 @@ class _SetupViewState extends State<SetupView> {
|
||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).welcomeInstruction),
|
AppLocalizations.of(context).welcomeInstruction,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -172,7 +198,9 @@ 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, fontWeight: FontWeight.bold),
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyWidget: Column(
|
bodyWidget: Column(
|
||||||
|
@ -213,14 +241,17 @@ 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) {
|
||||||
balance = double.parse(t);
|
final b = double.tryParse(t);
|
||||||
|
if (b == null) return;
|
||||||
|
balance = b;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -236,7 +267,9 @@ 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, fontWeight: FontWeight.bold),
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
bodyWidget: Column(
|
bodyWidget: Column(
|
||||||
|
@ -250,26 +283,70 @@ 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) => ListTile(
|
itemBuilder: (context, i) => (i == 0)
|
||||||
|
? const SizedBox()
|
||||||
|
: ListTile(
|
||||||
leading: GestureDetector(
|
leading: GestureDetector(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var icon = await FlutterIconPicker.showIconPicker(
|
final icon =
|
||||||
context);
|
await FlutterIconPicker.showIconPicker(
|
||||||
if (icon == null) return;
|
context,
|
||||||
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:
|
color: categories[i].color,
|
||||||
Theme.of(context).colorScheme.secondary),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
categories[i].icon,
|
categories[i].icon,
|
||||||
color:
|
color: categories[i]
|
||||||
Theme.of(context).colorScheme.onSecondary,
|
.color
|
||||||
|
.calculateTextColor(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -283,43 +360,52 @@ class _SetupViewState extends State<SetupView> {
|
||||||
),
|
),
|
||||||
title: GestureDetector(
|
title: GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
var controller = TextEditingController(
|
final 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) return;
|
if (controller.text.isEmpty) {
|
||||||
categories[i].name = controller.text;
|
return;
|
||||||
|
}
|
||||||
|
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).cancel),
|
AppLocalizations.of(context)
|
||||||
|
.cancel,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
title: AppLocalizations.of(context)
|
title: AppLocalizations.of(context)
|
||||||
.setupCategoriesEditingName,
|
.setupCategoriesEditingName,
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 400,
|
width: 400,
|
||||||
child:
|
child: PlatformField(
|
||||||
PlatformField(controller: controller),
|
controller: controller,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
categories[i].name,
|
categories[i].name,
|
||||||
style:
|
style: const TextStyle(
|
||||||
const TextStyle(fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -328,7 +414,7 @@ class _SetupViewState extends State<SetupView> {
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
var id = 1;
|
var id = 0;
|
||||||
while (categories
|
while (categories
|
||||||
.where((element) => element.id == id)
|
.where((element) => element.id == id)
|
||||||
.isNotEmpty) {
|
.isNotEmpty) {
|
||||||
|
@ -338,16 +424,20 @@ 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(Icons.question_mark.codePoint,
|
icon: IconData(
|
||||||
fontFamily: 'MaterialIcons'),
|
Icons.question_mark.codePoint,
|
||||||
|
fontFamily: 'MaterialIcons',
|
||||||
|
),
|
||||||
|
color: Colors.blueGrey.harmonizeWith(
|
||||||
|
Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
72
pubspec.lock
72
pubspec.lock
|
@ -21,10 +21,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: archive
|
name: archive
|
||||||
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b"
|
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.9"
|
version: "3.4.10"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -317,10 +317,26 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fl_chart
|
name: fl_chart
|
||||||
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
|
sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.65.0"
|
version: "0.66.0"
|
||||||
|
flex_color_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flex_color_picker
|
||||||
|
sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.3.0"
|
||||||
|
flex_seed_scheme:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flex_seed_scheme
|
||||||
|
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -450,6 +466,14 @@ 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:
|
||||||
|
@ -531,26 +555,26 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker
|
name: image_picker
|
||||||
sha256: fc712337719239b0b6e41316aa133350b078fa39b6cbd706b61f3fd421b03c77
|
sha256: "340efe08645537d6b088a30620ee5752298b1630f23a829181172610b868262b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.6"
|
||||||
image_picker_android:
|
image_picker_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: ecdc963d2aa67af5195e723a40580f802d4392e31457a12a562b3e2bd6a396fe
|
sha256: "1a27bf4cc0330389cebe465bab08fe6dec97e44015b4899637344bb7297759ec"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.9+1"
|
version: "0.8.9+2"
|
||||||
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: "50bc9ae6a77eea3a8b11af5eb6c661eeb858fdd2f734c2a4fd17086922347ef7"
|
sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -579,10 +603,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_platform_interface
|
name: image_picker_platform_interface
|
||||||
sha256: ed9b00e63977c93b0d2d2b343685bed9c324534ba5abafbb3dfbd6a780b1b514
|
sha256: "0e827c156e3a90edd3bbe7f6de048b39247b16e58173b08a835b7eb00aba239e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.9.1"
|
version: "2.9.2"
|
||||||
image_picker_windows:
|
image_picker_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -760,10 +784,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
|
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -816,10 +840,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: plugin_platform_interface
|
name: plugin_platform_interface
|
||||||
sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.7"
|
version: "2.1.8"
|
||||||
pointycastle:
|
pointycastle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1105,6 +1129,14 @@ 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:
|
||||||
|
@ -1157,18 +1189,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32
|
name: win32
|
||||||
sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574
|
sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.2.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xdg_directories
|
name: xdg_directories
|
||||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.0.4"
|
||||||
xml:
|
xml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
58
pubspec.yaml
58
pubspec.yaml
|
@ -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+2
|
version: 1.0.0-alpha+3
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.1.0-262.2.beta <4.0.0'
|
sdk: '>=3.1.0-262.2.beta <4.0.0'
|
||||||
|
@ -13,37 +13,35 @@ 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
|
||||||
fl_chart: ^0.65.0
|
flutter_slidable: ^3.0.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:
|
||||||
flutter_test:
|
build_runner: ^2.4.6
|
||||||
sdk: flutter
|
flutter_launcher_icons: ^0.13.1
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -51,11 +49,12 @@ 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
|
||||||
build_runner: ^2.4.6
|
flutter_test:
|
||||||
test: ^1.24.6
|
sdk: flutter
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_launcher_icons: ^0.13.1
|
test: ^1.24.6
|
||||||
|
very_good_analysis: ^5.1.0
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
android: true
|
android: true
|
||||||
|
@ -77,7 +76,6 @@ 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
|
||||||
|
@ -88,18 +86,14 @@ 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
|
||||||
|
|
Loading…
Reference in a new issue