Merge branch 'dev' into dev-graph
This commit is contained in:
commit
2d43ad5886
29 changed files with 1278 additions and 703 deletions
|
@ -1,3 +1,7 @@
|
|||
# 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
|
||||
# 1.0.0-alpha+2
|
||||
- Fixed localization issues
|
||||
- Added graphs for expenses and income per month/year
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
include: package:very_good_analysis/analysis_options.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
|
@ -23,6 +23,8 @@ linter:
|
|||
rules:
|
||||
# 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: false
|
||||
flutter_style_todos: false
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
|
@ -6,24 +6,28 @@ part 'category.g.dart';
|
|||
|
||||
/// Represents a category in a user's wallet
|
||||
class WalletCategory {
|
||||
final EntryType type;
|
||||
String name;
|
||||
final int id;
|
||||
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
|
||||
IconData icon;
|
||||
|
||||
WalletCategory(
|
||||
{required this.name,
|
||||
required this.type,
|
||||
/// Represents a category in a user's wallet
|
||||
WalletCategory({
|
||||
required this.name,
|
||||
required this.id,
|
||||
required this.icon});
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
||||
/// factory.
|
||||
/// Connects generated fromJson method
|
||||
factory WalletCategory.fromJson(Map<String, dynamic> 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;
|
||||
|
||||
/// Connects generated toJson method
|
||||
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
|
||||
|
||||
@override
|
||||
|
@ -34,7 +38,15 @@ class WalletCategory {
|
|||
|
||||
Map<String, dynamic> _iconDataToJson(IconData icon) =>
|
||||
{'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?);
|
||||
|
||||
/// Type of entry, either expense or income
|
||||
enum EntryType {
|
||||
/// Expense
|
||||
expense,
|
||||
|
||||
/// Income
|
||||
income
|
||||
}
|
||||
|
|
|
@ -9,20 +9,13 @@ part of 'category.dart';
|
|||
WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) =>
|
||||
WalletCategory(
|
||||
name: json['name'] as String,
|
||||
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
|
||||
id: json['id'] as int,
|
||||
icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) =>
|
||||
<String, dynamic>{
|
||||
'type': _$EntryTypeEnumMap[instance.type]!,
|
||||
'name': instance.name,
|
||||
'id': instance.id,
|
||||
'icon': _iconDataToJson(instance.icon),
|
||||
};
|
||||
|
||||
const _$EntryTypeEnumMap = {
|
||||
EntryType.expense: 'expense',
|
||||
EntryType.income: 'income',
|
||||
};
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'entry_data.g.dart';
|
||||
|
||||
/// Contains raw data
|
||||
@JsonSerializable()
|
||||
class EntryData {
|
||||
String name;
|
||||
String description;
|
||||
double amount;
|
||||
|
||||
/// Contains raw data
|
||||
EntryData({required this.name, required this.amount, this.description = ""});
|
||||
|
||||
/// Connects generated fromJson method
|
||||
factory EntryData.fromJson(Map<String, dynamic> 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);
|
||||
}
|
||||
|
|
|
@ -2,35 +2,51 @@ import 'package:currency_picker/currency_picker.dart';
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
part 'wallet.g.dart';
|
||||
|
||||
Currency _currencyFromJson(Map<String, dynamic> data) =>
|
||||
Currency.from(json: data);
|
||||
|
||||
/// Represents a single wallet
|
||||
///
|
||||
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
|
||||
@JsonSerializable()
|
||||
class Wallet {
|
||||
final String name;
|
||||
final List<WalletCategory> categories;
|
||||
final List<WalletSingleEntry> entries;
|
||||
double starterBalance;
|
||||
@JsonKey(fromJson: _currencyFromJson)
|
||||
final Currency currency;
|
||||
|
||||
Wallet(
|
||||
{required this.name,
|
||||
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
|
||||
Wallet({
|
||||
required this.name,
|
||||
required this.currency,
|
||||
this.categories = const [],
|
||||
this.entries = const [],
|
||||
this.starterBalance = 0});
|
||||
this.starterBalance = 0,
|
||||
});
|
||||
|
||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
||||
/// factory.
|
||||
/// Connects generated fromJson method
|
||||
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
|
||||
|
||||
/// Connect the generated [_$PersonToJson] function to the `toJson` method.
|
||||
/// 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);
|
||||
|
||||
/// 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 {
|
||||
var id = 1;
|
||||
while (entries.where((element) => element.id == id).isNotEmpty) {
|
||||
|
@ -39,13 +55,41 @@ class Wallet {
|
|||
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;
|
||||
}
|
||||
|
||||
/// 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(
|
||||
name: "Empty",
|
||||
currency: Currency.from(
|
||||
json: {
|
||||
"code": "USD",
|
||||
"name": "United States Dollar",
|
||||
"symbol": "\$",
|
||||
"symbol": r"$",
|
||||
"flag": "USD",
|
||||
"decimal_digits": 2,
|
||||
"number": 840,
|
||||
|
|
|
@ -1,30 +1,41 @@
|
|||
import 'package:prasule/api/category.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
|
||||
part 'walletentry.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
/// This is an entry containing a single item
|
||||
class WalletSingleEntry {
|
||||
EntryType type;
|
||||
EntryData data;
|
||||
DateTime date;
|
||||
WalletCategory category;
|
||||
int id;
|
||||
|
||||
WalletSingleEntry(
|
||||
{required this.data,
|
||||
/// This is an entry containing a single item
|
||||
WalletSingleEntry({
|
||||
required this.data,
|
||||
required this.type,
|
||||
required this.date,
|
||||
required this.category,
|
||||
required this.id});
|
||||
required this.id,
|
||||
});
|
||||
|
||||
/// Connect the generated [_$WalletEntry] function to the `fromJson`
|
||||
/// factory.
|
||||
/// Connects generated fromJson method
|
||||
factory WalletSingleEntry.fromJson(Map<String, dynamic> 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);
|
||||
}
|
||||
|
|
|
@ -3,42 +3,52 @@ import 'dart:io';
|
|||
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
|
||||
/// Used for [Wallet]-managing operations
|
||||
class WalletManager {
|
||||
/// Returns a list of all [Wallet]s
|
||||
static Future<List<Wallet>> listWallets() async {
|
||||
var path =
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
var wallets = <Wallet>[];
|
||||
for (var w in path.listSync().map((e) => e.path.split("/").last).toList()) {
|
||||
final wallets = <Wallet>[];
|
||||
for (final w
|
||||
in path.listSync().map((e) => e.path.split("/").last).toList()) {
|
||||
try {
|
||||
wallets.add(await loadWallet(w));
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
// TODO: do something with unreadable wallets
|
||||
}
|
||||
}
|
||||
logger.i(wallets.length);
|
||||
return wallets;
|
||||
}
|
||||
|
||||
/// Loads and returns a single [Wallet] by name
|
||||
static Future<Wallet> loadWallet(String name) async {
|
||||
var path =
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
var wallet = File("${path.path}/$name");
|
||||
final wallet = File("${path.path}/$name");
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
if (!wallet.existsSync()) {
|
||||
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 {
|
||||
var path =
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
var wallet = File("${path.path}/${w.name}");
|
||||
final wallet = File("${path.path}/${w.name}");
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
|
@ -47,10 +57,10 @@ class WalletManager {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Deletes the corresponding [Wallet] file
|
||||
static Future<void> deleteWallet(Wallet w) async {
|
||||
var path =
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
var wallet = File("${path.path}/${w.name}");
|
||||
wallet.deleteSync();
|
||||
File("${path.path}/${w.name}").deleteSync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,10 @@
|
|||
"barChart":"Sloupcový",
|
||||
"selectType":"Zvolte typ",
|
||||
"enableYou":"Povolit Material You (Může vyžadovat restart aplikace)",
|
||||
"enableYouDesc":"Aplikace použije barevné schéma z vaší tapety"
|
||||
"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"
|
||||
|
||||
}
|
|
@ -154,5 +154,9 @@
|
|||
"barChart":"Bar chart",
|
||||
"selectType":"Select type",
|
||||
"enableYou":"Enable Material You (May require an app restart)",
|
||||
"enableYouDesc":"The app will use a color scheme from your wallpaper"
|
||||
"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"
|
||||
}
|
|
@ -3,25 +3,37 @@ import 'dart:io';
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:prasule/util/color_schemes.g.dart';
|
||||
import 'package:prasule/views/home.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
var _materialYou = false;
|
||||
void main() async {
|
||||
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;
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
/// Global logger for debugging
|
||||
final logger = Logger();
|
||||
|
||||
/// The application itself
|
||||
class MyApp extends StatelessWidget {
|
||||
/// The application itself
|
||||
const MyApp({super.key});
|
||||
|
||||
/// If Material You was applied
|
||||
///
|
||||
/// Used to check if it is supported
|
||||
static bool appliedYou = false;
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
|
@ -35,21 +47,21 @@ class MyApp extends StatelessWidget {
|
|||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
...GlobalCupertinoLocalizations.delegates
|
||||
...GlobalCupertinoLocalizations.delegates,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
title: 'Prašule',
|
||||
theme: ThemeData(
|
||||
colorScheme: (_materialYou)
|
||||
colorScheme: _materialYou
|
||||
? light ?? lightColorScheme
|
||||
: lightColorScheme,
|
||||
useMaterial3: true,
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: (_materialYou)
|
||||
? dark ?? darkColorScheme
|
||||
: darkColorScheme),
|
||||
colorScheme:
|
||||
_materialYou ? dark ?? darkColorScheme : darkColorScheme,
|
||||
),
|
||||
home: const HomeView(),
|
||||
);
|
||||
},
|
||||
|
@ -57,10 +69,11 @@ class MyApp extends StatelessWidget {
|
|||
: Theme(
|
||||
data: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: (MediaQuery.of(context).platformBrightness ==
|
||||
Brightness.dark)
|
||||
colorScheme:
|
||||
(MediaQuery.of(context).platformBrightness == Brightness.dark)
|
||||
? darkColorScheme
|
||||
: lightColorScheme),
|
||||
: lightColorScheme,
|
||||
),
|
||||
child: const CupertinoApp(
|
||||
title: 'Prašule',
|
||||
home: HomeView(),
|
||||
|
|
|
@ -4,40 +4,48 @@ import 'package:dio/dio.dart';
|
|||
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
|
||||
/// Used for communication with my repo mirror
|
||||
///
|
||||
/// Downloads Tessdata for OCR
|
||||
class TessdataApi {
|
||||
static final Dio _client = Dio(
|
||||
BaseOptions(
|
||||
validateStatus: (status) => true,
|
||||
),
|
||||
);
|
||||
|
||||
/// Gets available languages from the repo
|
||||
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",
|
||||
options: Options(headers: {"Accept": "application/json"}));
|
||||
options: Options(headers: {"Accept": "application/json"}),
|
||||
);
|
||||
if ((res.statusCode ?? 500) > 399) {
|
||||
return Future.error("The server returned status code ${res.statusCode}");
|
||||
}
|
||||
var data = res.data;
|
||||
final data = res.data;
|
||||
final dataFiles = <String>[];
|
||||
for (var file in data) {
|
||||
if (!file["name"].endsWith(".traineddata")) continue;
|
||||
dataFiles.add(file["name"].replaceAll(".traineddata", ""));
|
||||
for (final file in data ?? <Map<String, dynamic>>[]) {
|
||||
if (!(file["name"] as String).endsWith(".traineddata")) continue;
|
||||
dataFiles.add((file["name"] as String).replaceAll(".traineddata", ""));
|
||||
}
|
||||
return dataFiles;
|
||||
}
|
||||
|
||||
/// Deletes data from device
|
||||
static Future<void> deleteData(String name) async {
|
||||
var dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
final dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
if (!dataDir.existsSync()) {
|
||||
dataDir.createSync();
|
||||
}
|
||||
var dataFile = File("${dataDir.path}/$name.traineddata");
|
||||
final dataFile = File("${dataDir.path}/$name.traineddata");
|
||||
if (!dataFile.existsSync()) return;
|
||||
dataFile.deleteSync();
|
||||
}
|
||||
|
||||
/// Finds existing data on the device
|
||||
static Future<List<String>> getDownloadedData() async {
|
||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
if (!tessDir.existsSync()) {
|
||||
tessDir.createSync();
|
||||
}
|
||||
|
@ -48,25 +56,29 @@ class TessdataApi {
|
|||
.toList();
|
||||
}
|
||||
|
||||
static Future<void> downloadData(String isoCode,
|
||||
{void Function(int, int)? callback}) async {
|
||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
/// Downloads data from the repo to the device
|
||||
static Future<void> downloadData(
|
||||
String isoCode, {
|
||||
void Function(int, int)? callback,
|
||||
}) async {
|
||||
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
if (!tessDir.existsSync()) {
|
||||
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?
|
||||
var res = await _client.get(
|
||||
final res = await _client.get<List<int>>(
|
||||
"https://git.mnau.xyz/hernik/tessdata_fast/raw/branch/main/$isoCode.traineddata",
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
onReceiveProgress: callback);
|
||||
onReceiveProgress: callback,
|
||||
);
|
||||
if ((res.statusCode ?? 500) > 399) {
|
||||
return Future.error("The server returned status code ${res.statusCode}");
|
||||
}
|
||||
try {
|
||||
var writefile = file.openSync(mode: FileMode.write);
|
||||
writefile.writeFromSync(res.data);
|
||||
writefile.closeSync();
|
||||
file.openSync(mode: FileMode.write)
|
||||
..writeFromSync(res.data!)
|
||||
..closeSync();
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
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/material.dart';
|
||||
import 'package:prasule/pw/platformwidget.dart';
|
||||
|
||||
/// A [PlatformWidget] implementation of a text field
|
||||
class PlatformButton extends PlatformWidget<TextButton, CupertinoButton> {
|
||||
const PlatformButton({
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
super.key,
|
||||
this.style,
|
||||
});
|
||||
final String text;
|
||||
final void Function()? onPressed;
|
||||
final ButtonStyle? style;
|
||||
const PlatformButton(
|
||||
{super.key, required this.text, required this.onPressed, this.style});
|
||||
|
||||
@override
|
||||
TextButton createAndroidWidget(BuildContext context) => TextButton(
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prasule/pw/platformwidget.dart';
|
||||
|
||||
/// A [PlatformWidget] implementation of a dialog
|
||||
class PlatformDialog extends PlatformWidget<AlertDialog, CupertinoAlertDialog> {
|
||||
const PlatformDialog(
|
||||
{required this.title, super.key, this.content, this.actions = const [],});
|
||||
final String title;
|
||||
final Widget? content;
|
||||
final List<Widget> actions;
|
||||
const PlatformDialog(
|
||||
{super.key, required this.title, this.content, this.actions = const []});
|
||||
|
||||
@override
|
||||
AlertDialog createAndroidWidget(BuildContext context) => AlertDialog(
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:prasule/pw/platformwidget.dart';
|
||||
|
||||
/// A [PlatformWidget] implementation of a text field
|
||||
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 bool? enabled;
|
||||
final bool obscureText;
|
||||
|
@ -16,20 +34,6 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
final TextStyle? textStyle;
|
||||
final TextAlign textAlign;
|
||||
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
|
||||
TextField createAndroidWidget(BuildContext context) => TextField(
|
||||
|
@ -39,7 +43,8 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
obscureText: obscureText,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(4))),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
autocorrect: autocorrect,
|
||||
keyboardType: keyboardType,
|
||||
style: textStyle,
|
||||
|
|
|
@ -3,7 +3,10 @@ import 'dart:io';
|
|||
import 'package:flutter/cupertino.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)
|
||||
? CupertinoPageRoute<T>(builder: builder)
|
||||
: MaterialPageRoute<T>(builder: builder);
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
|||
/// Abstract class used to create widgets for the respective platform UI library
|
||||
abstract class PlatformWidget<A extends Widget, I extends Widget>
|
||||
extends StatelessWidget {
|
||||
/// Abstract class used to create widgets for the respective platform UI library
|
||||
const PlatformWidget({super.key});
|
||||
|
||||
@override
|
||||
|
@ -16,7 +17,9 @@ abstract class PlatformWidget<A extends Widget, I extends Widget>
|
|||
}
|
||||
}
|
||||
|
||||
/// The widget that will be shown on Android
|
||||
A createAndroidWidget(BuildContext context);
|
||||
|
||||
/// The widget that will be shown on iOS
|
||||
I createIosWidget(BuildContext context);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const lightColorScheme = ColorScheme(
|
||||
|
|
|
@ -2,39 +2,57 @@ import 'package:currency_picker/currency_picker.dart';
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Monthly/Yearly expense/income [LineChart]
|
||||
class ExpensesLineChart extends StatelessWidget {
|
||||
const ExpensesLineChart(
|
||||
{super.key,
|
||||
/// Monthly/Yearly expense/income [LineChart]
|
||||
const ExpensesLineChart({
|
||||
required this.date,
|
||||
required this.locale,
|
||||
required this.expenseData,
|
||||
required this.incomeData,
|
||||
required this.currency,
|
||||
this.yearly = false});
|
||||
super.key,
|
||||
this.yearly = false,
|
||||
});
|
||||
|
||||
/// 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;
|
||||
List<double> get expenseDataSorted {
|
||||
var list = List<double>.from(expenseData);
|
||||
list.sort((a, b) => a.compareTo(b));
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Expense data, but sorted
|
||||
List<double> get expenseDataSorted =>
|
||||
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
||||
|
||||
/// Income data used for the graph
|
||||
final List<double> incomeData;
|
||||
List<double> get incomeDataSorted {
|
||||
var list = List<double>.from(incomeData);
|
||||
list.sort((a, b) => a.compareTo(b));
|
||||
return list;
|
||||
}
|
||||
|
||||
/// Income data, but sorted
|
||||
List<double> get incomeDataSorted =>
|
||||
List<double>.from(incomeData)..sort((a, b) => a.compareTo(b));
|
||||
|
||||
/// Calculates maxY for the graph
|
||||
double get maxY {
|
||||
if (incomeData.isEmpty) return expenseDataSorted.last;
|
||||
if (expenseData.isEmpty) return incomeDataSorted.last;
|
||||
|
@ -57,35 +75,45 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
(spots[index].barIndex == 0)
|
||||
? (yearly
|
||||
? AppLocalizations.of(context).incomeForMonth(
|
||||
DateFormat.MMMM(locale).format(DateTime(
|
||||
date.year, spots[index].x.toInt() + 1, 1)),
|
||||
DateFormat.MMMM(locale).format(
|
||||
DateTime(
|
||||
date.year,
|
||||
spots[index].x.toInt() + 1,
|
||||
),
|
||||
),
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(spots[index].y))
|
||||
name: currency.name,
|
||||
).format(spots[index].y),
|
||||
)
|
||||
: AppLocalizations.of(context).incomeForDay(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(spots[index].y),
|
||||
name: currency.name,
|
||||
).format(spots[index].y),
|
||||
))
|
||||
: (yearly
|
||||
? AppLocalizations.of(context).expensesForMonth(
|
||||
DateFormat.MMMM(locale).format(DateTime(
|
||||
date.year, spots[index].x.toInt() + 1, 1)),
|
||||
DateFormat.MMMM(locale).format(
|
||||
DateTime(
|
||||
date.year,
|
||||
spots[index].x.toInt() + 1,
|
||||
),
|
||||
),
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(spots[index].y))
|
||||
name: currency.name,
|
||||
).format(spots[index].y),
|
||||
)
|
||||
: AppLocalizations.of(context).expensesForDay(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(spots[index].y),
|
||||
name: currency.name,
|
||||
).format(spots[index].y),
|
||||
)),
|
||||
TextStyle(color: spots[index].bar.color),
|
||||
),
|
||||
|
@ -93,7 +121,7 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
maxY: maxY,
|
||||
maxX: (yearly) ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
|
||||
maxX: yearly ? 12 : DateTime(date.year, date.month, 0).day.toDouble(),
|
||||
minY: 0,
|
||||
minX: 0,
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
|
@ -104,11 +132,11 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
barWidth: 8,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
belowBarData: BarAreaData(),
|
||||
color: Colors.green
|
||||
.harmonizeWith(Theme.of(context).colorScheme.secondary),
|
||||
spots: List.generate(
|
||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
yearly ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
(index) => FlSpot(index.toDouble(), incomeData[index]),
|
||||
),
|
||||
),
|
||||
|
@ -118,22 +146,18 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
barWidth: 8,
|
||||
isStrokeCapRound: true,
|
||||
dotData: const FlDotData(show: false),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
belowBarData: BarAreaData(),
|
||||
color: Colors.red
|
||||
.harmonizeWith(Theme.of(context).colorScheme.secondary),
|
||||
spots: List.generate(
|
||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
yearly ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
(index) => FlSpot(index.toDouble() + 1, expenseData[index]),
|
||||
),
|
||||
),
|
||||
], // actual data
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(),
|
||||
topTitles: const AxisTitles(),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
reservedSize: 30,
|
||||
|
@ -142,13 +166,15 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
String text;
|
||||
if (yearly) {
|
||||
text = DateFormat.MMM(locale).format(
|
||||
DateTime(date.year, value.toInt() + 1, 1),
|
||||
DateTime(date.year, value.toInt() + 1),
|
||||
);
|
||||
} else {
|
||||
text = (value.toInt() + 1).toString();
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide, child: Text(text));
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(text),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -158,34 +184,52 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Renders expenses/income as a [BarChart]
|
||||
class ExpensesBarChart extends StatelessWidget {
|
||||
const ExpensesBarChart(
|
||||
{super.key,
|
||||
/// Renders expenses/income as a [BarChart]
|
||||
const ExpensesBarChart({
|
||||
required this.yearly,
|
||||
required this.date,
|
||||
required this.locale,
|
||||
required this.expenseData,
|
||||
required this.incomeData,
|
||||
required this.currency});
|
||||
final bool yearly;
|
||||
final DateTime date;
|
||||
final String locale;
|
||||
final List<double> expenseData;
|
||||
List<double> get expenseDataSorted {
|
||||
var list = List<double>.from(expenseData);
|
||||
list.sort((a, b) => a.compareTo(b));
|
||||
return list;
|
||||
}
|
||||
required this.currency,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// 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 List<double> incomeData;
|
||||
List<double> get incomeDataSorted {
|
||||
var list = List<double>.from(incomeData);
|
||||
list.sort((a, b) => a.compareTo(b));
|
||||
return list;
|
||||
}
|
||||
/// Expense data, but sorted
|
||||
List<double> get expenseDataSorted =>
|
||||
List<double>.from(expenseData)..sort((a, b) => a.compareTo(b));
|
||||
|
||||
/// Income data used for the graph
|
||||
final List<double> incomeData;
|
||||
|
||||
/// 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;
|
||||
|
@ -203,26 +247,28 @@ class ExpensesBarChart extends StatelessWidget {
|
|||
enabled: true,
|
||||
touchTooltipData: BarTouchTooltipData(
|
||||
getTooltipItem: (group, groupIndex, rod, rodIndex) =>
|
||||
(yearly) // create custom tooltips for graph bars
|
||||
yearly // create custom tooltips for graph bars
|
||||
? BarTooltipItem(
|
||||
(rodIndex == 1)
|
||||
? AppLocalizations.of(context).expensesForMonth(
|
||||
DateFormat.MMMM(locale).format(
|
||||
DateTime(date.year, groupIndex + 1, 1)),
|
||||
DateTime(date.year, groupIndex + 1),
|
||||
),
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(rod.toY),
|
||||
name: currency.name,
|
||||
).format(rod.toY),
|
||||
)
|
||||
: AppLocalizations.of(context).incomeForMonth(
|
||||
DateFormat.MMMM(locale).format(
|
||||
DateTime(date.year, groupIndex + 1, 1)),
|
||||
DateTime(date.year, groupIndex + 1),
|
||||
),
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(rod.toY),
|
||||
name: currency.name,
|
||||
).format(rod.toY),
|
||||
),
|
||||
TextStyle(color: rod.color),
|
||||
)
|
||||
|
@ -232,27 +278,23 @@ class ExpensesBarChart extends StatelessWidget {
|
|||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(rod.toY),
|
||||
name: currency.name,
|
||||
).format(rod.toY),
|
||||
)
|
||||
: AppLocalizations.of(context).incomeForDay(
|
||||
NumberFormat.compactCurrency(
|
||||
locale: locale,
|
||||
symbol: currency.symbol,
|
||||
name: currency.name)
|
||||
.format(rod.toY),
|
||||
name: currency.name,
|
||||
).format(rod.toY),
|
||||
),
|
||||
TextStyle(color: rod.color),
|
||||
),
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false),
|
||||
),
|
||||
rightTitles: const AxisTitles(),
|
||||
topTitles: const AxisTitles(),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
|
@ -261,13 +303,15 @@ class ExpensesBarChart extends StatelessWidget {
|
|||
String text;
|
||||
if (yearly) {
|
||||
text = DateFormat.MMM(locale).format(
|
||||
DateTime(date.year, value.toInt() + 1, 1),
|
||||
DateTime(date.year, value.toInt() + 1),
|
||||
);
|
||||
} else {
|
||||
text = (value.toInt() + 1).toString();
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide, child: Text(text));
|
||||
axisSide: meta.axisSide,
|
||||
child: Text(text),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -275,7 +319,7 @@ class ExpensesBarChart extends StatelessWidget {
|
|||
minY: 0,
|
||||
maxY: maxY,
|
||||
barGroups: List<BarChartGroupData>.generate(
|
||||
(yearly) ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
yearly ? 12 : DateTime(date.year, date.month, 0).day,
|
||||
(index) => BarChartGroupData(
|
||||
x: index,
|
||||
barRods: [
|
||||
|
|
|
@ -1,18 +1,26 @@
|
|||
import 'package:flutter/material.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/entry_data.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/// Used when user wants to add new entry
|
||||
class CreateEntryView extends StatefulWidget {
|
||||
/// Used when user wants to add new entry
|
||||
const CreateEntryView({required this.w, 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 WalletSingleEntry? editEntry;
|
||||
const CreateEntryView({super.key, required this.w, this.editEntry});
|
||||
|
||||
@override
|
||||
State<CreateEntryView> createState() => _CreateEntryViewState();
|
||||
|
@ -31,7 +39,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
|||
data: EntryData(amount: 0, name: ""),
|
||||
type: EntryType.expense,
|
||||
date: DateTime.now(),
|
||||
category: widget.w.categories.first);
|
||||
category: widget.w.categories.first,
|
||||
);
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
@ -68,12 +77,14 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
|||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).amount,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.amount.toString()),
|
||||
text: newEntry.data.amount.toString(),
|
||||
),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'\d+[\.,]{0,1}\d{0,}'))
|
||||
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
newEntry.data.amount = double.parse(v);
|
||||
|
@ -159,7 +170,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
|||
constraints: BoxConstraints(
|
||||
minWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||
maxHeight: 300),
|
||||
maxHeight: 300,
|
||||
),
|
||||
child: PlatformField(
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
|
@ -181,8 +193,8 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
|||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context).errorEmptyName),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).errorEmptyName),
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
@ -196,7 +208,7 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
|||
(value) => Navigator.of(context).pop(widget.w),
|
||||
); // TODO loading circle?
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.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.dart';
|
||||
|
@ -8,12 +11,13 @@ import 'package:prasule/pw/platformbutton.dart';
|
|||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/util/drawer.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/setup.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
class GraphView extends StatefulWidget {
|
||||
/// Shows data from a [Wallet] in graphs
|
||||
const GraphView({super.key});
|
||||
|
||||
@override
|
||||
|
@ -25,8 +29,8 @@ class _GraphViewState extends State<GraphView> {
|
|||
Wallet? selectedWallet;
|
||||
List<Wallet> wallets = [];
|
||||
String? locale;
|
||||
var yearlyBtnSet = {"monthly"};
|
||||
var graphTypeSet = {"expense", "income"};
|
||||
Set<String> yearlyBtnSet = {"monthly"};
|
||||
Set<String> graphTypeSet = {"expense", "income"};
|
||||
bool get yearly => yearlyBtnSet.contains("yearly");
|
||||
|
||||
@override
|
||||
|
@ -36,23 +40,24 @@ class _GraphViewState extends State<GraphView> {
|
|||
}
|
||||
|
||||
List<double> generateChartData(EntryType type) {
|
||||
var data = List<double>.filled(
|
||||
(yearly)
|
||||
? 12
|
||||
: DateTime(_selectedDate.year, _selectedDate.month, 0).day,
|
||||
0.0);
|
||||
final data = List<double>.filled(
|
||||
yearly ? 12 : DateTime(_selectedDate.year, _selectedDate.month, 0).day,
|
||||
0,
|
||||
);
|
||||
if (selectedWallet == null) return [];
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
var entriesForRange = selectedWallet!.entries.where((element) =>
|
||||
final entriesForRange = selectedWallet!.entries.where(
|
||||
(element) =>
|
||||
((!yearly)
|
||||
? element.date.month == _selectedDate.month &&
|
||||
element.date.year == _selectedDate.year &&
|
||||
element.date.day == i + 1
|
||||
: element.date.month == i + 1 &&
|
||||
element.date.year == _selectedDate.year) &&
|
||||
element.type == type);
|
||||
element.type == type,
|
||||
);
|
||||
var sum = 0.0;
|
||||
for (var e in entriesForRange) {
|
||||
for (final e in entriesForRange) {
|
||||
sum += e.data.amount;
|
||||
}
|
||||
data[i] = sum;
|
||||
|
@ -60,11 +65,13 @@ class _GraphViewState extends State<GraphView> {
|
|||
return data;
|
||||
}
|
||||
|
||||
void loadWallet() async {
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (c) => const SetupView()));
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
selectedWallet = wallets.first;
|
||||
|
@ -101,7 +108,7 @@ class _GraphViewState extends State<GraphView> {
|
|||
DropdownMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).newWallet),
|
||||
)
|
||||
),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
if (v == null || v == -1) {
|
||||
|
@ -126,23 +133,24 @@ class _GraphViewState extends State<GraphView> {
|
|||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
AppLocalizations.of(context).settings,
|
||||
AppLocalizations.of(context).about
|
||||
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(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsView(),
|
||||
platformRoute(
|
||||
(context) => const SettingsView(),
|
||||
),
|
||||
);
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
showAboutDialog(
|
||||
context: context,
|
||||
applicationLegalese: AppLocalizations.of(context).license,
|
||||
applicationName: "Prašule");
|
||||
applicationName: "Prašule",
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
drawer: makeDrawer(context, 2),
|
||||
|
@ -193,8 +201,8 @@ class _GraphViewState extends State<GraphView> {
|
|||
selected: yearlyBtnSet,
|
||||
onSelectionChanged: (selection) async {
|
||||
yearlyBtnSet = selection;
|
||||
var s = await SharedPreferences.getInstance();
|
||||
chartType = (yearly)
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = yearly
|
||||
? (s.getInt("yearlygraph") ?? 1)
|
||||
: (s.getInt("monthlygraph") ?? 2);
|
||||
setState(() {});
|
||||
|
@ -204,50 +212,54 @@ class _GraphViewState extends State<GraphView> {
|
|||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondaryContainer),
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
children: [
|
||||
PlatformButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context).colorScheme.primary),
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimary)),
|
||||
text: (yearly)
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
text: yearly
|
||||
? DateFormat.y(locale).format(_selectedDate)
|
||||
: DateFormat.yMMMM(locale)
|
||||
.format(_selectedDate),
|
||||
onPressed: () async {
|
||||
var firstDate = (selectedWallet!.entries
|
||||
final firstDate = (selectedWallet!.entries
|
||||
..sort(
|
||||
(a, b) => a.date.compareTo(b.date)))
|
||||
(a, b) => a.date.compareTo(b.date),
|
||||
))
|
||||
.first
|
||||
.date;
|
||||
var lastDate = (selectedWallet!.entries
|
||||
final lastDate = (selectedWallet!.entries
|
||||
..sort(
|
||||
(a, b) => b.date.compareTo(a.date)))
|
||||
(a, b) => b.date.compareTo(a.date),
|
||||
))
|
||||
.first
|
||||
.date;
|
||||
logger.i(firstDate);
|
||||
logger.i(lastDate);
|
||||
var newDate = await showDatePicker(
|
||||
final newDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime(_selectedDate.year,
|
||||
_selectedDate.month, 1),
|
||||
initialDate: DateTime(
|
||||
_selectedDate.year,
|
||||
_selectedDate.month,
|
||||
),
|
||||
firstDate: firstDate,
|
||||
lastDate: lastDate,
|
||||
initialEntryMode: (yearly)
|
||||
initialEntryMode: yearly
|
||||
? DatePickerEntryMode.input
|
||||
: DatePickerEntryMode.calendar,
|
||||
initialDatePickerMode: (yearly)
|
||||
initialDatePickerMode: yearly
|
||||
? DatePickerMode.year
|
||||
: DatePickerMode.day);
|
||||
: DatePickerMode.day,
|
||||
);
|
||||
if (newDate == null) return;
|
||||
_selectedDate = newDate;
|
||||
setState(() {});
|
||||
|
@ -270,12 +282,14 @@ class _GraphViewState extends State<GraphView> {
|
|||
expenseData: (graphTypeSet
|
||||
.contains("expense"))
|
||||
? generateChartData(
|
||||
EntryType.expense)
|
||||
EntryType.expense,
|
||||
)
|
||||
: [],
|
||||
incomeData: (graphTypeSet
|
||||
.contains("income"))
|
||||
? generateChartData(
|
||||
EntryType.income)
|
||||
EntryType.income,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
: ExpensesLineChart(
|
||||
|
@ -286,19 +300,21 @@ class _GraphViewState extends State<GraphView> {
|
|||
expenseData: (graphTypeSet
|
||||
.contains("expense"))
|
||||
? generateChartData(
|
||||
EntryType.expense)
|
||||
EntryType.expense,
|
||||
)
|
||||
: [],
|
||||
incomeData: (graphTypeSet
|
||||
.contains("income"))
|
||||
? generateChartData(
|
||||
EntryType.income)
|
||||
EntryType.income,
|
||||
)
|
||||
: [],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
// ignore_for_file: inference_failure_on_function_invocation
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.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_speed_dial/flutter_speed_dial.dart';
|
||||
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
|
||||
|
@ -10,8 +15,8 @@ import 'package:intl/date_symbol_data_local.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletentry.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/network/tessdata.dart';
|
||||
|
@ -23,9 +28,10 @@ import 'package:prasule/views/create_entry.dart';
|
|||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/settings/tessdata_list.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/// Main view, shows entries
|
||||
class HomeView extends StatefulWidget {
|
||||
/// Main view, shows entries
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
|
@ -50,11 +56,13 @@ class _HomeViewState extends State<HomeView> {
|
|||
loadWallet();
|
||||
}
|
||||
|
||||
void loadWallet() async {
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (c) => const SetupView()));
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||
);
|
||||
return;
|
||||
}
|
||||
selectedWallet = wallets.first;
|
||||
|
@ -77,7 +85,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
// debug option to quickly fill a wallet with data
|
||||
if (selectedWallet == null) return;
|
||||
selectedWallet!.entries.clear();
|
||||
var random = Random();
|
||||
final random = Random();
|
||||
for (var i = 0; i < 30; i++) {
|
||||
selectedWallet!.entries.add(
|
||||
WalletSingleEntry(
|
||||
|
@ -116,7 +124,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
child: const Icon(Icons.edit),
|
||||
label: AppLocalizations.of(context).addNew,
|
||||
onTap: () async {
|
||||
var sw = await Navigator.of(context).push<Wallet>(
|
||||
final sw = await Navigator.of(context).push<Wallet>(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => CreateEntryView(w: selectedWallet!),
|
||||
),
|
||||
|
@ -125,14 +133,14 @@ class _HomeViewState extends State<HomeView> {
|
|||
selectedWallet = sw;
|
||||
}
|
||||
setState(() {});
|
||||
}),
|
||||
},
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.camera_alt),
|
||||
label: AppLocalizations.of(context).addCamera,
|
||||
onTap: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? media =
|
||||
await picker.pickImage(source: ImageSource.camera);
|
||||
final picker = ImagePicker();
|
||||
final media = await picker.pickImage(source: ImageSource.camera);
|
||||
logger.i(media?.name);
|
||||
},
|
||||
),
|
||||
|
@ -161,7 +169,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
DropdownMenuItem(
|
||||
value: -1,
|
||||
child: Text(AppLocalizations.of(context).newWallet),
|
||||
)
|
||||
),
|
||||
],
|
||||
onChanged: (v) async {
|
||||
if (v == null || v == -1) {
|
||||
|
@ -186,23 +194,29 @@ class _HomeViewState extends State<HomeView> {
|
|||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
AppLocalizations.of(context).settings,
|
||||
AppLocalizations.of(context).about
|
||||
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(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsView(),
|
||||
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");
|
||||
applicationName: "Prašule",
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
|
@ -216,7 +230,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
width: 40,
|
||||
height: 40,
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
: (selectedWallet!.entries.isEmpty)
|
||||
|
@ -231,44 +245,46 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).noEntriesSub,
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
: GroupedListView(
|
||||
groupHeaderBuilder: (element) => Text(
|
||||
DateFormat.yMMMM(locale).format(element.date),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
elements: selectedWallet!.entries,
|
||||
itemComparator: (a, b) => b.date.compareTo(a.date),
|
||||
groupBy: (e) => DateFormat.yMMMM(locale).format(e.date),
|
||||
groupComparator: (a, b) {
|
||||
// TODO: better sorting algorithm lol
|
||||
var yearA = RegExp(r'\d+').firstMatch(a);
|
||||
final yearA = RegExp(r'\d+').firstMatch(a);
|
||||
if (yearA == null) return 0;
|
||||
var yearB = RegExp(r'\d+').firstMatch(b);
|
||||
final yearB = RegExp(r'\d+').firstMatch(b);
|
||||
if (yearB == null) return 0;
|
||||
var compareYears = int.parse(yearA.group(0)!)
|
||||
final compareYears = int.parse(yearA.group(0)!)
|
||||
.compareTo(int.parse(yearB.group(0)!));
|
||||
if (compareYears != 0) return compareYears;
|
||||
var months = List<String>.generate(
|
||||
final months = List<String>.generate(
|
||||
12,
|
||||
(index) => DateFormat.MMMM(locale).format(
|
||||
DateTime(2023, index + 1),
|
||||
),
|
||||
);
|
||||
var monthA = RegExp(r'[^0-9 ]+').firstMatch(a);
|
||||
final monthA = RegExp('[^0-9 ]+').firstMatch(a);
|
||||
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;
|
||||
return months.indexOf(monthB.group(0)!).compareTo(
|
||||
months.indexOf(monthA.group(0)!),
|
||||
);
|
||||
},
|
||||
itemBuilder: (context, element) => Slidable(
|
||||
endActionPane:
|
||||
ActionPane(motion: const ScrollMotion(), children: [
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (c) {
|
||||
Navigator.of(context)
|
||||
|
@ -309,15 +325,18 @@ class _HomeViewState extends State<HomeView> {
|
|||
title:
|
||||
AppLocalizations.of(context).sureDialog,
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteSure),
|
||||
AppLocalizations.of(context).deleteSure,
|
||||
),
|
||||
actions: [
|
||||
PlatformButton(
|
||||
text: AppLocalizations.of(context).yes,
|
||||
onPressed: () {
|
||||
selectedWallet?.entries.removeWhere(
|
||||
(e) => e.id == element.id);
|
||||
(e) => e.id == element.id,
|
||||
);
|
||||
WalletManager.saveWallet(
|
||||
selectedWallet!);
|
||||
selectedWallet!,
|
||||
);
|
||||
Navigator.of(cx).pop();
|
||||
setState(() {});
|
||||
},
|
||||
|
@ -333,14 +352,16 @@ class _HomeViewState extends State<HomeView> {
|
|||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.secondary),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
element.category.icon,
|
||||
color:
|
||||
|
@ -350,7 +371,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
title: Text(element.data.name),
|
||||
subtitle: Text(
|
||||
"${element.data.amount} ${selectedWallet!.currency.symbol}"),
|
||||
"${element.data.amount} ${selectedWallet!.currency.symbol}",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -360,7 +382,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
}
|
||||
|
||||
Future<void> startOcr(ImageSource imgSrc) async {
|
||||
var availableLanguages = await TessdataApi.getDownloadedData();
|
||||
final availableLanguages = await TessdataApi.getDownloadedData();
|
||||
if (availableLanguages.isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
@ -370,8 +392,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
label: AppLocalizations.of(context).download,
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => const TessdataListView(),
|
||||
platformRoute(
|
||||
(c) => const TessdataListView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -381,70 +403,82 @@ class _HomeViewState extends State<HomeView> {
|
|||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
var selectedLanguages = List<bool>.filled(availableLanguages.length, false);
|
||||
final selectedLanguages =
|
||||
List<bool>.filled(availableLanguages.length, false);
|
||||
selectedLanguages[0] = true;
|
||||
|
||||
showDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (c) => StatefulBuilder(
|
||||
builder: (ctx, setState) => PlatformDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? media = await picker.pickImage(source: imgSrc);
|
||||
final picker = ImagePicker();
|
||||
final media = await picker.pickImage(source: imgSrc);
|
||||
if (media == null) {
|
||||
if (mounted) Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
// get selected languages
|
||||
var selected = availableLanguages
|
||||
.where((element) =>
|
||||
selectedLanguages[availableLanguages.indexOf(element)])
|
||||
final selected = availableLanguages
|
||||
.where(
|
||||
(element) => selectedLanguages[
|
||||
availableLanguages.indexOf(element)],
|
||||
)
|
||||
.join("+")
|
||||
.replaceAll(".traineddata", "");
|
||||
logger.i(selected);
|
||||
if (!mounted) return;
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => PlatformDialog(
|
||||
title: AppLocalizations.of(context).ocrLoading),
|
||||
barrierDismissible: false);
|
||||
var string = await FlutterTesseractOcr.extractText(media.path,
|
||||
title: AppLocalizations.of(context).ocrLoading,
|
||||
),
|
||||
barrierDismissible: false,
|
||||
),
|
||||
);
|
||||
final string = await FlutterTesseractOcr.extractText(
|
||||
media.path,
|
||||
language: selected,
|
||||
args: {
|
||||
"psm": "4",
|
||||
"preserve_interword_spaces": "1",
|
||||
});
|
||||
},
|
||||
);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
logger.i(string);
|
||||
if (!mounted) return;
|
||||
var lines = string.split("\n")
|
||||
final lines = string.split("\n")
|
||||
..removeWhere((element) {
|
||||
element.trim();
|
||||
return element.isEmpty;
|
||||
});
|
||||
var price = 0.0;
|
||||
var description = "";
|
||||
for (var line in lines) {
|
||||
final description = StringBuffer();
|
||||
for (final line in lines) {
|
||||
// find numbered prices on each line
|
||||
var regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+');
|
||||
for (var match in regex.allMatches(line)) {
|
||||
final regex = RegExp(r'\d+(?:(?:\.|,) {0,}\d{0,})+');
|
||||
for (final match in regex.allMatches(line)) {
|
||||
price += double.tryParse(match.group(0).toString()) ?? 0;
|
||||
}
|
||||
description += "${line.replaceAll(regex, "")}\n";
|
||||
description.write("${line.replaceAll(regex, "")}\n");
|
||||
}
|
||||
Navigator.of(ctx).pop();
|
||||
// show edit
|
||||
Navigator.of(context)
|
||||
.push<WalletSingleEntry>(
|
||||
final newEntry =
|
||||
await Navigator.of(context).push<WalletSingleEntry>(
|
||||
platformRoute<WalletSingleEntry>(
|
||||
(c) => CreateEntryView(
|
||||
w: selectedWallet!,
|
||||
editEntry: WalletSingleEntry(
|
||||
data: EntryData(
|
||||
name: "", amount: price, description: description),
|
||||
name: "",
|
||||
amount: price,
|
||||
description: description.toString(),
|
||||
),
|
||||
type: EntryType.expense,
|
||||
date: DateTime.now(),
|
||||
category: selectedWallet!.categories.first,
|
||||
|
@ -452,17 +486,12 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.then(
|
||||
(newEntry) {
|
||||
// save entry if we didn't return empty
|
||||
);
|
||||
if (newEntry == null) return;
|
||||
selectedWallet!.entries.add(newEntry);
|
||||
WalletManager.saveWallet(selectedWallet!);
|
||||
await WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Text("Ok"),
|
||||
),
|
||||
TextButton(
|
||||
|
@ -495,10 +524,10 @@ class _HomeViewState extends State<HomeView> {
|
|||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Text(availableLanguages[index].split(".").first)
|
||||
Text(availableLanguages[index].split(".").first),
|
||||
],
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -507,12 +536,12 @@ class _HomeViewState extends State<HomeView> {
|
|||
}
|
||||
|
||||
Future<void> getLostData() async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final LostDataResponse response = await picker.retrieveLostData();
|
||||
final picker = ImagePicker();
|
||||
final response = await picker.retrieveLostData();
|
||||
if (response.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final List<XFile>? files = response.files;
|
||||
final files = response.files;
|
||||
if (files != null) {
|
||||
logger.i("Found lost files");
|
||||
_handleLostFiles(files);
|
||||
|
|
244
lib/views/settings/edit_categories.dart
Normal file
244
lib/views/settings/edit_categories.dart
Normal file
|
@ -0,0 +1,244 @@
|
|||
// 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_iconpicker/flutter_iconpicker.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/walletmanager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformdialog.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/views/settings/settings.dart';
|
||||
import 'package:prasule/views/setup.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) return;
|
||||
selectedWallet!.categories[i].icon = icon;
|
||||
await WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
selectedWallet!.categories[i].icon,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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',
|
||||
),
|
||||
),
|
||||
);
|
||||
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_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/pw/platformdialog.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Allows setting the type of graph for certain data
|
||||
class GraphTypeSettingsView extends StatefulWidget {
|
||||
/// Allows setting the type of graph for certain data
|
||||
const GraphTypeSettingsView({super.key});
|
||||
|
||||
@override
|
||||
|
@ -34,15 +38,18 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
applicationType: ApplicationType.both,
|
||||
darkTheme: SettingsThemeData(
|
||||
settingsListBackground: Theme.of(context).colorScheme.background,
|
||||
titleTextColor: Theme.of(context).colorScheme.primary),
|
||||
titleTextColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
sections: [
|
||||
SettingsSection(
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
title: Text(AppLocalizations.of(context).yearly),
|
||||
value: Text(_yearly == 1
|
||||
value: Text(
|
||||
_yearly == 1
|
||||
? AppLocalizations.of(context).barChart
|
||||
: AppLocalizations.of(context).lineChart),
|
||||
: AppLocalizations.of(context).lineChart,
|
||||
),
|
||||
onPressed: (c) => showDialog(
|
||||
context: c,
|
||||
builder: (ctx) => PlatformDialog(
|
||||
|
@ -53,13 +60,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
width: MediaQuery.of(ctx).size.width,
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(AppLocalizations.of(context).barChart,
|
||||
textAlign: TextAlign.center),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).barChart,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
var s = await SharedPreferences.getInstance();
|
||||
s.setInt("yearlygraph", 1);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
await s.setInt("yearlygraph", 1);
|
||||
_yearly = 1;
|
||||
if (!mounted) return;
|
||||
Navigator.of(ctx).pop();
|
||||
|
@ -71,15 +80,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
width: MediaQuery.of(context).size.width,
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).lineChart,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
var s = await SharedPreferences.getInstance();
|
||||
s.setInt("yearlygraph", 2);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
await s.setInt("yearlygraph", 2);
|
||||
_yearly = 2;
|
||||
if (!mounted) return;
|
||||
Navigator.of(ctx).pop();
|
||||
|
@ -94,9 +103,11 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
),
|
||||
SettingsTile.navigation(
|
||||
title: Text(AppLocalizations.of(context).monthly),
|
||||
value: Text(_monthly == 1
|
||||
value: Text(
|
||||
_monthly == 1
|
||||
? AppLocalizations.of(context).barChart
|
||||
: AppLocalizations.of(context).lineChart),
|
||||
: AppLocalizations.of(context).lineChart,
|
||||
),
|
||||
onPressed: (c) => showDialog(
|
||||
context: c,
|
||||
builder: (ctx) => PlatformDialog(
|
||||
|
@ -107,15 +118,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
width: MediaQuery.of(ctx).size.width,
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).barChart,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
var s = await SharedPreferences.getInstance();
|
||||
s.setInt("monthlygraph", 1);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
await s.setInt("monthlygraph", 1);
|
||||
_monthly = 1;
|
||||
if (!mounted) return;
|
||||
Navigator.of(ctx).pop();
|
||||
|
@ -127,14 +138,15 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
width: MediaQuery.of(ctx).size.width,
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).lineChart,
|
||||
textAlign: TextAlign.center),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
var s = await SharedPreferences.getInstance();
|
||||
s.setInt("monthlygraph", 2);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
await s.setInt("monthlygraph", 2);
|
||||
_monthly = 2;
|
||||
if (!mounted) return;
|
||||
Navigator.of(ctx).pop();
|
||||
|
@ -148,7 +160,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:prasule/main.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/tessdata_list.dart';
|
||||
import 'package:settings_ui/settings_ui.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Shows settings categories
|
||||
class SettingsView extends StatefulWidget {
|
||||
/// Shows settings categories
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
|
@ -36,8 +39,25 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
applicationType: ApplicationType.both,
|
||||
darkTheme: SettingsThemeData(
|
||||
settingsListBackground: Theme.of(context).colorScheme.background,
|
||||
titleTextColor: Theme.of(context).colorScheme.primary),
|
||||
titleTextColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
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(
|
||||
tiles: [
|
||||
SettingsTile.navigation(
|
||||
|
@ -45,9 +65,12 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
description:
|
||||
Text(AppLocalizations.of(context).downloadedOcrDesc),
|
||||
trailing: const Icon(Icons.keyboard_arrow_right),
|
||||
onPressed: (context) => Navigator.of(context)
|
||||
.push(platformRoute((c) => const TessdataListView())),
|
||||
)
|
||||
onPressed: (context) => Navigator.of(context).push(
|
||||
platformRoute(
|
||||
(c) => const TessdataListView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
title: Text(AppLocalizations.of(context).ocr),
|
||||
),
|
||||
|
@ -68,16 +91,18 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
SettingsTile.switchTile(
|
||||
initialValue: _useMaterialYou,
|
||||
onToggle: (v) async {
|
||||
var s = await SharedPreferences.getInstance();
|
||||
s.setBool("useMaterialYou", v);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
await s.setBool("useMaterialYou", v);
|
||||
_useMaterialYou = v;
|
||||
setState(() {});
|
||||
},
|
||||
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:io';
|
||||
|
||||
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:prasule/main.dart';
|
||||
import 'package:prasule/network/tessdata.dart';
|
||||
import 'package:prasule/pw/platformbutton.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 {
|
||||
/// Used to manage downloaded Tessdata for OCR
|
||||
const TessdataListView({super.key});
|
||||
|
||||
@override
|
||||
|
@ -18,7 +22,7 @@ class TessdataListView extends StatefulWidget {
|
|||
|
||||
class _TessdataListViewState extends State<TessdataListView> {
|
||||
final _tessdata = [
|
||||
{"eng": true}
|
||||
{"eng": true},
|
||||
];
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
|
@ -49,19 +53,22 @@ class _TessdataListViewState extends State<TessdataListView> {
|
|||
itemBuilder: (context, i) => ListTile(
|
||||
title: Text(_tessdata[i].keys.first),
|
||||
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).download),
|
||||
: AppLocalizations.of(context).download,
|
||||
),
|
||||
onPressed: () async {
|
||||
var lang = _tessdata[i].keys.first;
|
||||
final lang = _tessdata[i].keys.first;
|
||||
if (_tessdata[i][lang]!) {
|
||||
// deleting data
|
||||
showDialog(
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => PlatformDialog(
|
||||
title: AppLocalizations.of(context).sureDialog,
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.deleteOcr(lang)),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).deleteOcr(lang),
|
||||
),
|
||||
actions: [
|
||||
PlatformButton(
|
||||
text: AppLocalizations.of(context).yes,
|
||||
|
@ -86,8 +93,9 @@ class _TessdataListViewState extends State<TessdataListView> {
|
|||
|
||||
// TODO: handle wifi errors
|
||||
//* downloading traineddata
|
||||
var progressStream = StreamController<double>();
|
||||
final progressStream = StreamController<double>();
|
||||
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => PlatformDialog(
|
||||
|
@ -102,16 +110,21 @@ class _TessdataListViewState extends State<TessdataListView> {
|
|||
if (snapshot.hasError) {
|
||||
return const Text("Error");
|
||||
}
|
||||
return Text(AppLocalizations.of(context)
|
||||
.langDownloadProgress(snapshot.data!));
|
||||
return Text(
|
||||
AppLocalizations.of(context)
|
||||
.langDownloadProgress(snapshot.data!),
|
||||
);
|
||||
},
|
||||
stream: progressStream.stream,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await TessdataApi.downloadData(lang, callback: (a, b) {
|
||||
await TessdataApi.downloadData(
|
||||
lang,
|
||||
callback: (a, b) {
|
||||
if (progressStream.isClosed) return;
|
||||
var p = a / b * 1000;
|
||||
final p = a / b * 1000;
|
||||
progressStream.add(p.roundToDouble() / 10);
|
||||
if (p / 10 >= 100) {
|
||||
logger.i("Done");
|
||||
|
@ -121,7 +134,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
|||
progressStream.close();
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
@ -134,25 +148,26 @@ class _TessdataListViewState extends State<TessdataListView> {
|
|||
|
||||
/// Used to find which `.traineddata` is already downloaded and which not
|
||||
/// so we can show it to the user
|
||||
void loadAllTessdata() async {
|
||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
var d = await TessdataApi.getAvailableData();
|
||||
var dataStatus = <Map<String, bool>>[];
|
||||
for (var data in d) {
|
||||
var e = <String, bool>{};
|
||||
Future<void> loadAllTessdata() async {
|
||||
final tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||
final d = await TessdataApi.getAvailableData();
|
||||
final dataStatus = <Map<String, bool>>[];
|
||||
for (final data in d) {
|
||||
final e = <String, bool>{};
|
||||
e[data] = false;
|
||||
dataStatus.add(e);
|
||||
}
|
||||
var appDir = tessDir.listSync();
|
||||
for (var file in appDir) {
|
||||
final appDir = tessDir.listSync();
|
||||
for (final file in appDir) {
|
||||
if (file is! File ||
|
||||
!file.path.endsWith("traineddata") ||
|
||||
file.path.endsWith("eng.traineddata")) continue;
|
||||
logger.i(file.path);
|
||||
var filename = file.path.split("/").last;
|
||||
dataStatus[dataStatus.indexWhere((element) =>
|
||||
element.keys.first == filename.replaceAll(".traineddata", ""))]
|
||||
[filename.replaceAll(".traineddata", "")] = true;
|
||||
final filename = file.path.split("/").last;
|
||||
dataStatus[dataStatus.indexWhere(
|
||||
(element) =>
|
||||
element.keys.first == filename.replaceAll(".traineddata", ""),
|
||||
)][filename.replaceAll(".traineddata", "")] = true;
|
||||
}
|
||||
_tessdata.addAll(dataStatus);
|
||||
setState(() {});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
// ignore_for_file: inference_failure_on_function_invocation
|
||||
|
||||
import 'package:currency_picker/currency_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:flutter_iconpicker/flutter_iconpicker.dart';
|
||||
import 'package:introduction_screen/introduction_screen.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
|
@ -9,10 +12,12 @@ import 'package:prasule/api/walletmanager.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/views/home.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
/// View that shows on first-time setup
|
||||
class SetupView extends StatefulWidget {
|
||||
/// View that shows on first-time setup
|
||||
const SetupView({super.key, this.newWallet = false});
|
||||
|
||||
/// We are only creating a new wallet, no first-time setup
|
||||
|
@ -22,10 +27,11 @@ class SetupView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SetupViewState extends State<SetupView> {
|
||||
var _selectedCurrency = Currency.from(json: {
|
||||
var _selectedCurrency = Currency.from(
|
||||
json: {
|
||||
"code": "USD",
|
||||
"name": "United States Dollar",
|
||||
"symbol": "\$",
|
||||
"symbol": r"$",
|
||||
"flag": "USD",
|
||||
"decimal_digits": 2,
|
||||
"number": 840,
|
||||
|
@ -34,40 +40,47 @@ class _SetupViewState extends State<SetupView> {
|
|||
"decimal_separator": ".",
|
||||
"space_between_amount_and_symbol": false,
|
||||
"symbol_on_left": true,
|
||||
});
|
||||
var categories = <WalletCategory>[];
|
||||
var name = "";
|
||||
var balance = 0.0;
|
||||
},
|
||||
);
|
||||
List<WalletCategory> categories = <WalletCategory>[];
|
||||
String name = "";
|
||||
double balance = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (categories.isEmpty) {
|
||||
categories = [
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).noCategory,
|
||||
id: 0,
|
||||
icon: IconData(
|
||||
Icons.payments.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryHealth,
|
||||
type: EntryType.expense,
|
||||
id: 1,
|
||||
icon: IconData(Icons.medical_information.codePoint,
|
||||
fontFamily: 'MaterialIcons'),
|
||||
icon: IconData(
|
||||
Icons.medical_information.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryCar,
|
||||
type: EntryType.expense,
|
||||
id: 2,
|
||||
icon:
|
||||
IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryFood,
|
||||
type: EntryType.expense,
|
||||
id: 3,
|
||||
icon:
|
||||
IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryTravel,
|
||||
type: EntryType.expense,
|
||||
id: 4,
|
||||
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
|
||||
),
|
||||
|
@ -87,9 +100,7 @@ class _SetupViewState extends State<SetupView> {
|
|||
dotsDecorator: DotsDecorator(
|
||||
activeColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
showNextButton: true,
|
||||
showBackButton: true,
|
||||
showDoneButton: true,
|
||||
next: Text(AppLocalizations.of(context).next),
|
||||
back: Text(AppLocalizations.of(context).back),
|
||||
done: Text(AppLocalizations.of(context).finish),
|
||||
|
@ -97,15 +108,18 @@ class _SetupViewState extends State<SetupView> {
|
|||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars(); // TODO: iOS replacement
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).errorEmptyName)));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).errorEmptyName),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
var wallet = Wallet(
|
||||
final wallet = Wallet(
|
||||
name: name,
|
||||
currency: _selectedCurrency,
|
||||
categories: categories);
|
||||
categories: categories,
|
||||
);
|
||||
WalletManager.saveWallet(wallet).then(
|
||||
(value) {
|
||||
if (!value) {
|
||||
|
@ -123,8 +137,8 @@ class _SetupViewState extends State<SetupView> {
|
|||
return;
|
||||
}
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (c) => const HomeView(),
|
||||
platformRoute(
|
||||
(c) => const HomeView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
@ -139,7 +153,9 @@ class _SetupViewState extends State<SetupView> {
|
|||
child: Text(
|
||||
AppLocalizations.of(context).welcome,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
|
@ -150,7 +166,8 @@ class _SetupViewState extends State<SetupView> {
|
|||
if (!widget.newWallet)
|
||||
Flexible(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).welcomeAboutPrasule),
|
||||
AppLocalizations.of(context).welcomeAboutPrasule,
|
||||
),
|
||||
),
|
||||
if (!widget.newWallet)
|
||||
const SizedBox(
|
||||
|
@ -158,7 +175,8 @@ class _SetupViewState extends State<SetupView> {
|
|||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).welcomeInstruction),
|
||||
AppLocalizations.of(context).welcomeInstruction,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -172,7 +190,9 @@ class _SetupViewState extends State<SetupView> {
|
|||
AppLocalizations.of(context).setupWalletNameCurrency,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
bodyWidget: Column(
|
||||
|
@ -213,11 +233,12 @@ class _SetupViewState extends State<SetupView> {
|
|||
labelText:
|
||||
AppLocalizations.of(context).setupStartingBalance,
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true),
|
||||
decimal: true,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(
|
||||
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
|
||||
)
|
||||
),
|
||||
],
|
||||
onChanged: (t) {
|
||||
balance = double.parse(t);
|
||||
|
@ -236,7 +257,9 @@ class _SetupViewState extends State<SetupView> {
|
|||
AppLocalizations.of(context).setupCategoriesHeading,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, fontWeight: FontWeight.bold),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
bodyWidget: Column(
|
||||
|
@ -250,11 +273,15 @@ class _SetupViewState extends State<SetupView> {
|
|||
height: MediaQuery.of(context).size.height * 0.64,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, i) => ListTile(
|
||||
itemBuilder: (context, i) => (i == 0)
|
||||
? const SizedBox()
|
||||
: ListTile(
|
||||
leading: GestureDetector(
|
||||
onTap: () async {
|
||||
var icon = await FlutterIconPicker.showIconPicker(
|
||||
context);
|
||||
final icon =
|
||||
await FlutterIconPicker.showIconPicker(
|
||||
context,
|
||||
);
|
||||
if (icon == null) return;
|
||||
categories[i].icon = icon;
|
||||
setState(() {});
|
||||
|
@ -262,14 +289,17 @@ class _SetupViewState extends State<SetupView> {
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color:
|
||||
Theme.of(context).colorScheme.secondary),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.secondary,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
categories[i].icon,
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSecondary,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -283,43 +313,49 @@ class _SetupViewState extends State<SetupView> {
|
|||
),
|
||||
title: GestureDetector(
|
||||
onTap: () {
|
||||
var controller = TextEditingController(
|
||||
text: categories[i].name);
|
||||
final controller = TextEditingController(
|
||||
text: categories[i].name,
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => PlatformDialog(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
if (controller.text.isEmpty) return;
|
||||
categories[i].name = controller.text;
|
||||
if (controller.text.isEmpty)
|
||||
return;
|
||||
categories[i].name =
|
||||
controller.text;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).ok),
|
||||
AppLocalizations.of(context).ok,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).cancel),
|
||||
AppLocalizations.of(context)
|
||||
.cancel,
|
||||
),
|
||||
),
|
||||
],
|
||||
title: AppLocalizations.of(context)
|
||||
.setupCategoriesEditingName,
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child:
|
||||
PlatformField(controller: controller),
|
||||
child: PlatformField(
|
||||
controller: controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
categories[i].name,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -328,7 +364,7 @@ class _SetupViewState extends State<SetupView> {
|
|||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
var id = 1;
|
||||
var id = 0;
|
||||
while (categories
|
||||
.where((element) => element.id == id)
|
||||
.isNotEmpty) {
|
||||
|
@ -338,16 +374,17 @@ class _SetupViewState extends State<SetupView> {
|
|||
WalletCategory(
|
||||
name: AppLocalizations.of(context)
|
||||
.setupWalletNamePlaceholder,
|
||||
type: EntryType.expense,
|
||||
id: id,
|
||||
icon: IconData(Icons.question_mark.codePoint,
|
||||
fontFamily: 'MaterialIcons'),
|
||||
icon: IconData(
|
||||
Icons.question_mark.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
),
|
||||
);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
12
pubspec.lock
12
pubspec.lock
|
@ -317,10 +317,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "5a74434cc83bf64346efb562f1a06eefaf1bcb530dc3d96a104f631a1eff8d79"
|
||||
sha256: fe6fec7d85975a99c73b9515a69a6e291364accfa0e4a5b3ce6de814d74b9a1c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.65.0"
|
||||
version: "0.66.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -1105,6 +1105,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
56
pubspec.yaml
56
pubspec.yaml
|
@ -1,7 +1,7 @@
|
|||
name: prasule
|
||||
description: Open-source private expense tracker
|
||||
|
||||
version: 1.0.0-alpha.2+2
|
||||
version: 1.0.0-alpha+3
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.0-262.2.beta <4.0.0'
|
||||
|
@ -13,37 +13,33 @@ environment:
|
|||
# the latest version available on pub.dev. To see which dependencies have newer
|
||||
# versions available, run `flutter pub outdated`.
|
||||
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
|
||||
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
|
||||
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:
|
||||
sdk: flutter
|
||||
fl_chart: ^0.65.0
|
||||
flutter_slidable: ^3.0.0
|
||||
flutter_speed_dial: ^7.0.0
|
||||
flutter_tesseract_ocr: ^0.4.23
|
||||
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
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.6
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
|
@ -51,11 +47,12 @@ dev_dependencies:
|
|||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^3.0.0
|
||||
build_runner: ^2.4.6
|
||||
test: ^1.24.6
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
test: ^1.24.6
|
||||
very_good_analysis: ^5.1.0
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
|
@ -77,7 +74,6 @@ flutter_launcher_icons:
|
|||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
generate: true
|
||||
|
@ -88,18 +84,14 @@ flutter:
|
|||
assets:
|
||||
- assets/
|
||||
- assets/tessdata/
|
||||
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
# assets:
|
||||
# - images/a_dot_burr.jpeg
|
||||
# - images/a_dot_ham.jpeg
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
||||
# For details regarding adding assets from package dependencies, see
|
||||
# https://flutter.dev/assets-and-images/#from-packages
|
||||
|
||||
# To add custom fonts to your application, add a fonts section here,
|
||||
# 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
|
||||
|
|
Loading…
Reference in a new issue