Compare commits
8 commits
e68bd611bc
...
9fac138386
Author | SHA1 | Date | |
---|---|---|---|
9fac138386 | |||
|
2d50509163 | ||
ef52caa836 | |||
300f359070 | |||
6530b58c19 | |||
2339739ff8 | |||
5f99e35709 | |||
fd04bc1dc4 |
28 changed files with 1243 additions and 775 deletions
|
@ -1,6 +1,8 @@
|
||||||
# newVersion
|
# 2.0.0
|
||||||
- Upgrade dependencies
|
- Upgrade dependencies
|
||||||
- Use less `await`s in WalletManager class
|
- Use less `await`s in WalletManager class
|
||||||
|
- Added debt management
|
||||||
|
- Changed android app ID
|
||||||
|
|
||||||
# 1.1.1
|
# 1.1.1
|
||||||
- Removed deprecated code
|
- Removed deprecated code
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# prasule
|
# prasule
|
||||||
[![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/status_badge.svg)](https://codemagic.io/apps/64faee78aae8c48abc70dbc6/64faee78aae8c48abc70dbc5/latest_build) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![wakatime](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c.svg)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
|
[![Coverage](https://sq.mnau.xyz/api/project_badges/measure?project=prasule&metric=coverage&token=sqb_098860bff1a308465ca633fe569ec3d24c9d8e4b)](https://sq.mnau.xyz/dashboard?id=prasule) [![Maintainability Rating](https://sq.mnau.xyz/api/project_badges/measure?project=prasule&metric=sqale_rating&token=sqb_098860bff1a308465ca633fe569ec3d24c9d8e4b)](https://sq.mnau.xyz/dashboard?id=prasule) [![Bug issue count](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgit.mnau.xyz%2Fapi%2Fv1%2Frepos%2Fhernik%2Fprasule%2Fissues%3Flabels%3DKind%2FBug&query=%24.length&logo=forgejo&label=bug%20issues&color=red)](https://git.mnau.xyz/hernik/prasule/issues?q=&type=all&sort=&state=open&labels=144&milestone=0&project=0&assignee=0&poster=0) [![Time spent on project](https://wt.mnau.xyz/api/badge/hernik/interval:today/project:prasule)](https://wakatime.com/badge/user/17178fab-a33c-430f-a764-7b3f26c7b966/project/bf1f40b0-c8c0-4f72-8ad6-c861ecdcc90c) [![Translation status](https://hosted.weblate.org/widget/prasule/svg-badge.svg)](https://hosted.weblate.org/engage/prasule/) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) [![Codemagic build status](https://api.codemagic.io/apps/64faee78aae8c48abc70dbc6/6698dab78938fa88eaef9f82/status_badge.svg)](https://codemagic.io/app/64faee78aae8c48abc70dbc6/6698dab78938fa88eaef9f82/latest_build)
|
||||||
|
|
||||||
Expense manager
|
Expense manager
|
||||||
|
|
||||||
- [Apple Testflight](https://testflight.apple.com/join/C22pcnPc)
|
- [Apple Testflight](https://testflight.apple.com/join/C22pcnPc)
|
||||||
- [Google Play beta testing](https://play.google.com/store/apps/details?id=cafe.caras.prasule)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
``` Prašule - simple, private & open-source expense tracker
|
``` Prašule - simple, private & open-source expense tracker
|
||||||
|
|
|
@ -48,7 +48,7 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "cafe.caras.prasule"
|
applicationId "wtf.caras.prasule"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
|
|
|
@ -37,6 +37,8 @@ class DebtEntry {
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
/// List of people who payed
|
/// List of people who payed
|
||||||
@JsonKey(defaultValue: DebtPerson.unknownPerson)
|
@JsonKey(defaultValue: _defaultDebtPayers)
|
||||||
List<DebtPerson> whoPayed;
|
final List<DebtPerson> whoPayed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<DebtPerson> _defaultDebtPayers() => [DebtPerson.unknownPerson()];
|
||||||
|
|
|
@ -19,7 +19,7 @@ DebtEntry _$DebtEntryFromJson(Map<String, dynamic> json) {
|
||||||
whoPayed: (json['whoPayed'] as List<dynamic>?)
|
whoPayed: (json['whoPayed'] as List<dynamic>?)
|
||||||
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
|
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
DebtPerson.unknownPerson(),
|
_defaultDebtPayers(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ part 'debt_person.g.dart';
|
||||||
/// Represents a single person in a debt scenario
|
/// Represents a single person in a debt scenario
|
||||||
class DebtPerson {
|
class DebtPerson {
|
||||||
/// Represents a single person in a debt scenario
|
/// Represents a single person in a debt scenario
|
||||||
DebtPerson({required this.id, required this.name});
|
DebtPerson({required this.id, required this.name, this.bankAccount});
|
||||||
|
|
||||||
/// Default [DebtPerson] instance for json_serializable
|
/// Default [DebtPerson] instance for json_serializable
|
||||||
factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown");
|
factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown");
|
||||||
|
@ -25,4 +25,9 @@ class DebtPerson {
|
||||||
/// Identifier that the user will see
|
/// Identifier that the user will see
|
||||||
@JsonKey(defaultValue: "Unknown")
|
@JsonKey(defaultValue: "Unknown")
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
/// Person's bank account
|
||||||
|
///
|
||||||
|
/// Used to generate a QR code payment
|
||||||
|
String? bankAccount;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ DebtPerson _$DebtPersonFromJson(Map<String, dynamic> json) {
|
||||||
return DebtPerson(
|
return DebtPerson(
|
||||||
id: (json['id'] as num).toInt(),
|
id: (json['id'] as num).toInt(),
|
||||||
name: json['name'] as String? ?? 'Unknown',
|
name: json['name'] as String? ?? 'Unknown',
|
||||||
|
bankAccount: json['bankAccount'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,4 +23,5 @@ Map<String, dynamic> _$DebtPersonToJson(DebtPerson instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
|
'bankAccount': instance.bankAccount,
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,8 +11,8 @@ class DebtScenario {
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.isArchived,
|
required this.isArchived,
|
||||||
|
required this.people,
|
||||||
this.entries = const [],
|
this.entries = const [],
|
||||||
this.people = const [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Generates a class instance from a Map
|
/// Generates a class instance from a Map
|
||||||
|
@ -22,7 +22,7 @@ class DebtScenario {
|
||||||
/// Converts the data in this instance into a Map
|
/// Converts the data in this instance into a Map
|
||||||
Map<String, dynamic> toJson() => _$DebtScenarioToJson(this);
|
Map<String, dynamic> toJson() => _$DebtScenarioToJson(this);
|
||||||
|
|
||||||
/// Unique identified
|
/// Unique identifier
|
||||||
@JsonKey(disallowNullValue: true)
|
@JsonKey(disallowNullValue: true)
|
||||||
final int id;
|
final int id;
|
||||||
|
|
||||||
|
@ -36,11 +36,29 @@ class DebtScenario {
|
||||||
|
|
||||||
/// All entries
|
/// All entries
|
||||||
@JsonKey(defaultValue: [])
|
@JsonKey(defaultValue: [])
|
||||||
List<DebtEntry> entries;
|
final List<DebtEntry> entries;
|
||||||
|
|
||||||
/// All people
|
/// All people
|
||||||
@JsonKey(defaultValue: _defaultPeopleList)
|
@JsonKey(defaultValue: _defaultPeopleList)
|
||||||
List<DebtPerson> people;
|
final List<DebtPerson> people;
|
||||||
|
|
||||||
|
/// Getter for the next unused unique number ID for a [DebtPerson]
|
||||||
|
int get nextPersonId {
|
||||||
|
var id = 1;
|
||||||
|
while (people.where((element) => element.id == id).isNotEmpty) {
|
||||||
|
id++; // create unique ID
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Getter for the next unused unique number ID for a [DebtEntry]
|
||||||
|
int get nextEntryId {
|
||||||
|
var id = 1;
|
||||||
|
while (entries.where((element) => element.id == id).isNotEmpty) {
|
||||||
|
id++; // create unique ID
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DebtPerson> _defaultPeopleList() => [DebtPerson.unknownPerson()];
|
List<DebtPerson> _defaultPeopleList() => [DebtPerson.unknownPerson()];
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:prasule/api/category.dart';
|
import 'package:prasule/api/category.dart';
|
||||||
|
import 'package:prasule/api/debt_scenario.dart';
|
||||||
import 'package:prasule/api/entry_data.dart';
|
import 'package:prasule/api/entry_data.dart';
|
||||||
import 'package:prasule/api/recurring_entry.dart';
|
import 'package:prasule/api/recurring_entry.dart';
|
||||||
import 'package:prasule/api/wallet_entry.dart';
|
import 'package:prasule/api/wallet_entry.dart';
|
||||||
|
@ -31,6 +32,7 @@ class Wallet {
|
||||||
this.categories = const [],
|
this.categories = const [],
|
||||||
this.entries = const [],
|
this.entries = const [],
|
||||||
this.recurringEntries = const [],
|
this.recurringEntries = const [],
|
||||||
|
this.debts = const [],
|
||||||
this.starterBalance = 0,
|
this.starterBalance = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,6 +55,10 @@ class Wallet {
|
||||||
@JsonKey(defaultValue: [])
|
@JsonKey(defaultValue: [])
|
||||||
final List<WalletSingleEntry> entries;
|
final List<WalletSingleEntry> entries;
|
||||||
|
|
||||||
|
/// List of user's [DebtScenario]s
|
||||||
|
@JsonKey(defaultValue: [])
|
||||||
|
final List<DebtScenario> debts;
|
||||||
|
|
||||||
/// The starting balance of the wallet
|
/// The starting balance of the wallet
|
||||||
///
|
///
|
||||||
/// Used to calculate current balance
|
/// Used to calculate current balance
|
||||||
|
@ -88,6 +94,16 @@ class Wallet {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Getter for the next unused unique number ID in the wallet's **debts**
|
||||||
|
/// list
|
||||||
|
int get nextDebtId {
|
||||||
|
var id = 0;
|
||||||
|
while (debts.where((element) => element.id == id).isNotEmpty) {
|
||||||
|
id++; // create unique ID
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
/// Handles adding recurring entries to the entry list
|
/// Handles adding recurring entries to the entry list
|
||||||
void recurEntries() {
|
void recurEntries() {
|
||||||
final n = DateTime.now();
|
final n = DateTime.now();
|
||||||
|
|
|
@ -25,6 +25,10 @@ Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
|
||||||
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
|
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
[],
|
[],
|
||||||
|
debts: (json['debts'] as List<dynamic>?)
|
||||||
|
?.map((e) => DebtScenario.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
|
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -33,6 +37,7 @@ Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'categories': instance.categories,
|
'categories': instance.categories,
|
||||||
'entries': instance.entries,
|
'entries': instance.entries,
|
||||||
|
'debts': instance.debts,
|
||||||
'starterBalance': instance.starterBalance,
|
'starterBalance': instance.starterBalance,
|
||||||
'currency': instance.currency,
|
'currency': instance.currency,
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ import 'package:prasule/main.dart';
|
||||||
/// Used for [Wallet]-managing operations
|
/// Used for [Wallet]-managing operations
|
||||||
class WalletManager {
|
class WalletManager {
|
||||||
/// Currently selected wallet
|
/// Currently selected wallet
|
||||||
static Wallet? selectedWallet;
|
static late Wallet selectedWallet;
|
||||||
|
|
||||||
/// Path to the directory with wallet files
|
/// Path to the directory with wallet files
|
||||||
///
|
///
|
||||||
|
|
|
@ -122,5 +122,14 @@
|
||||||
"incomePerYearCategory":"Příjmy podle kategorie za rok {year}",
|
"incomePerYearCategory":"Příjmy podle kategorie za rok {year}",
|
||||||
"incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}",
|
"incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}",
|
||||||
"selectYear":"Zvolte rok",
|
"selectYear":"Zvolte rok",
|
||||||
"selectMonth":"Zvolte měsíc a rok"
|
"selectMonth":"Zvolte měsíc a rok",
|
||||||
|
"debts":"Dlužníček",
|
||||||
|
"debtNamePlaceholder":"Dluhy přátel",
|
||||||
|
"people":"Lidé",
|
||||||
|
"addSomePeople":"Přidej lidi pomocí tlačítka níže",
|
||||||
|
"bankAccount":"Číslo bankovního účtu",
|
||||||
|
"noDebtScenarios":"Žádné seznamy dlužníků :(",
|
||||||
|
"noDebtScenariosSub":"Nový můžete vytvořit pomocí plovoucího tlačítka.",
|
||||||
|
"noPersonError":"Musíte vložit alespoň jednoho člověka!",
|
||||||
|
"unnamed":"Bez jména"
|
||||||
}
|
}
|
|
@ -313,5 +313,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selectYear":"Select a year",
|
"selectYear":"Select a year",
|
||||||
"selectMonth":"Select a month and year"
|
"selectMonth":"Select a month and year",
|
||||||
|
"debts":"Debts",
|
||||||
|
"debtNamePlaceholder":"Friends' debts",
|
||||||
|
"people":"People",
|
||||||
|
"addSomePeople":"Add people using the button below",
|
||||||
|
"bankAccount":"Bank account number",
|
||||||
|
"noDebtScenarios":"No debt scenarios :(",
|
||||||
|
"noDebtScenariosSub":"Create one using the floating action button.",
|
||||||
|
"noPersonError":"You need to add at least one person!",
|
||||||
|
"unnamed":"Unnamed"
|
||||||
}
|
}
|
1
lib/l10n/app_sk.arb
Normal file
1
lib/l10n/app_sk.arb
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -10,26 +10,26 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:prasule/pw/platformwidget.dart';
|
import 'package:prasule/pw/platformwidget.dart';
|
||||||
|
|
||||||
/// A [PlatformWidget] implementation of a text field
|
/// A [PlatformWidget] implementation of a text field
|
||||||
class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
class PlatformField extends PlatformWidget<TextFormField, CupertinoTextField> {
|
||||||
const PlatformField({
|
const PlatformField(
|
||||||
super.key,
|
{super.key,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.enabled,
|
this.enabled,
|
||||||
this.labelText,
|
this.labelText,
|
||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
this.autocorrect = false,
|
this.autocorrect = false,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.inputFormatters = const [],
|
this.inputFormatters = const [],
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.autofillHints,
|
this.autofillHints,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
this.textAlign = TextAlign.start,
|
this.textAlign = TextAlign.start,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.inputBorder = const OutlineInputBorder(),
|
this.inputBorder = const OutlineInputBorder(),
|
||||||
this.suffix,
|
this.suffix,
|
||||||
this.prefix,
|
this.prefix,
|
||||||
});
|
this.validator});
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
final bool? enabled;
|
final bool? enabled;
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
|
@ -46,9 +46,10 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
||||||
final FocusNode? focusNode;
|
final FocusNode? focusNode;
|
||||||
final Widget? suffix;
|
final Widget? suffix;
|
||||||
final Widget? prefix;
|
final Widget? prefix;
|
||||||
|
final String? Function(String?)? validator;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextField createAndroidWidget(BuildContext context) => TextField(
|
TextFormField createAndroidWidget(BuildContext context) => TextFormField(
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
|
@ -67,6 +68,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
autofillHints: autofillHints,
|
autofillHints: autofillHints,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
validator: validator,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -5,12 +5,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:prasule/pw/platformroute.dart';
|
import 'package:prasule/pw/platformroute.dart';
|
||||||
import 'package:prasule/views/graph_view.dart';
|
import 'package:prasule/views/debts/debt_view.dart';
|
||||||
|
import 'package:prasule/views/graphs/graph_view.dart';
|
||||||
import 'package:prasule/views/home.dart';
|
import 'package:prasule/views/home.dart';
|
||||||
import 'package:prasule/views/recurring_view.dart';
|
import 'package:prasule/views/recurring/recurring_view.dart';
|
||||||
|
|
||||||
/// Makes the drawer because I won't enter the same code in every view
|
/// Makes the drawer because I won't enter the same code in every view
|
||||||
Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
Drawer makeDrawer(BuildContext context, Pages page) => Drawer(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
const DrawerHeader(child: Text("Prašule")),
|
const DrawerHeader(child: Text("Prašule")),
|
||||||
|
@ -19,9 +20,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).home,
|
AppLocalizations.of(context).home,
|
||||||
),
|
),
|
||||||
selected: page == 1,
|
selected: page == Pages.home,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (page == 1) {
|
if (page == Pages.home) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -34,9 +35,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).graphs,
|
AppLocalizations.of(context).graphs,
|
||||||
),
|
),
|
||||||
selected: page == 2,
|
selected: page == Pages.graphs,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (page == 2) {
|
if (page == Pages.graphs) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -49,9 +50,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).recurringPayments,
|
AppLocalizations.of(context).recurringPayments,
|
||||||
),
|
),
|
||||||
selected: page == 3,
|
selected: page == Pages.recurringEntries,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (page == 3) {
|
if (page == Pages.recurringEntries) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +61,37 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.people),
|
||||||
|
title: Text(
|
||||||
|
AppLocalizations.of(context).debts,
|
||||||
|
),
|
||||||
|
selected: page == Pages.debts,
|
||||||
|
onTap: () {
|
||||||
|
if (page == Pages.debts) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Navigator.of(context).pushReplacement(
|
||||||
|
platformRoute((p0) => const DebtView()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// All the pages that drawer can navigate to
|
||||||
|
enum Pages {
|
||||||
|
/// [HomeView]
|
||||||
|
home,
|
||||||
|
|
||||||
|
/// [GraphView]
|
||||||
|
graphs,
|
||||||
|
|
||||||
|
/// [RecurringEntriesView]
|
||||||
|
recurringEntries,
|
||||||
|
|
||||||
|
/// [DebtView]
|
||||||
|
debts
|
||||||
|
}
|
||||||
|
|
141
lib/views/debts/debt_view.dart
Normal file
141
lib/views/debts/debt_view.dart
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:prasule/api/debt_scenario.dart';
|
||||||
|
import 'package:prasule/api/wallet.dart';
|
||||||
|
import 'package:prasule/api/wallet_manager.dart';
|
||||||
|
import 'package:prasule/main.dart';
|
||||||
|
import 'package:prasule/pw/platformroute.dart';
|
||||||
|
import 'package:prasule/util/drawer.dart';
|
||||||
|
import 'package:prasule/views/debts/setup_debt_scenario.dart';
|
||||||
|
import 'package:prasule/views/setup.dart';
|
||||||
|
|
||||||
|
/// Shows the selected [DebtScenario]
|
||||||
|
class DebtView extends StatefulWidget {
|
||||||
|
/// Shows the selected [DebtScenario]
|
||||||
|
const DebtView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DebtView> createState() => _DebtViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DebtViewState extends State<DebtView> {
|
||||||
|
List<Wallet> wallets = [];
|
||||||
|
void loadWallet() {
|
||||||
|
wallets = WalletManager.listWallets();
|
||||||
|
if (wallets.isEmpty && mounted) {
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loadWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: WalletManager.selectedWallet.debts.isEmpty
|
||||||
|
? 1
|
||||||
|
: WalletManager.selectedWallet.debts.length,
|
||||||
|
child: Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(platformRoute((c) => const SetupDebtScenario()))
|
||||||
|
.then((v) {
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: DropdownButton<int>(
|
||||||
|
value: wallets.indexOf(
|
||||||
|
wallets
|
||||||
|
.where((w) => w.name == WalletManager.selectedWallet.name)
|
||||||
|
.first,
|
||||||
|
),
|
||||||
|
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 = WalletManager.listWallets();
|
||||||
|
logger.i(wallets.length);
|
||||||
|
WalletManager.selectedWallet = wallets.last;
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WalletManager.selectedWallet = wallets[v];
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
bottom: TabBar(
|
||||||
|
tabs: (WalletManager.selectedWallet.debts.isEmpty)
|
||||||
|
? [Text(AppLocalizations.of(context).welcome)]
|
||||||
|
: List.generate(
|
||||||
|
WalletManager.selectedWallet.debts.length,
|
||||||
|
(index) => Tab(
|
||||||
|
text: WalletManager.selectedWallet.debts[index].name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
drawer: makeDrawer(context, Pages.debts),
|
||||||
|
body: TabBarView(
|
||||||
|
children: (WalletManager.selectedWallet.debts.isEmpty)
|
||||||
|
? [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).noDebtScenarios,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(AppLocalizations.of(context).noDebtScenariosSub),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: List.generate(
|
||||||
|
WalletManager.selectedWallet.debts.length,
|
||||||
|
(c) => const Placeholder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
254
lib/views/debts/setup_debt_scenario.dart
Normal file
254
lib/views/debts/setup_debt_scenario.dart
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:prasule/api/debt_person.dart';
|
||||||
|
import 'package:prasule/api/debt_scenario.dart';
|
||||||
|
import 'package:prasule/api/wallet_manager.dart';
|
||||||
|
import 'package:prasule/pw/platformfield.dart';
|
||||||
|
import 'package:prasule/util/show_message.dart';
|
||||||
|
|
||||||
|
/// Used to create and/or edit [DebtScenario]s
|
||||||
|
class SetupDebtScenario extends StatefulWidget {
|
||||||
|
/// Used to create and/or edit [DebtScenario]s
|
||||||
|
const SetupDebtScenario({super.key, this.toEdit});
|
||||||
|
|
||||||
|
/// If not null, loads this [DebtScenario]'s data for editing
|
||||||
|
final DebtScenario? toEdit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SetupDebtScenario> createState() => _SetupDebtScenarioState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SetupDebtScenarioState extends State<SetupDebtScenario> {
|
||||||
|
late DebtScenario _scenario;
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final GlobalKey<FormState> _formKey = GlobalKey();
|
||||||
|
|
||||||
|
/// Stores data for each [ExpansionPanel]
|
||||||
|
final List<bool> _isOpen = [];
|
||||||
|
final List<TextEditingController> _panelControllers = [];
|
||||||
|
bool _isLoading = true;
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
if (!_isLoading) return;
|
||||||
|
_scenario = widget.toEdit ??
|
||||||
|
DebtScenario(
|
||||||
|
id: WalletManager.selectedWallet.nextDebtId,
|
||||||
|
name: AppLocalizations.of(context).debtNamePlaceholder,
|
||||||
|
isArchived: false,
|
||||||
|
people: [],
|
||||||
|
entries: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load stuff from the scenario
|
||||||
|
_nameController.text = _scenario.name;
|
||||||
|
_isOpen.addAll(List.filled(_scenario.people.length, false));
|
||||||
|
_panelControllers.addAll(
|
||||||
|
List.filled(_scenario.people.length * 2, TextEditingController()),
|
||||||
|
);
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.95,
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
child: PlatformField(
|
||||||
|
labelText: AppLocalizations.of(context).name,
|
||||||
|
controller: _nameController,
|
||||||
|
validator: (input) {
|
||||||
|
if (input == null || input.isEmpty) {
|
||||||
|
return AppLocalizations.of(context).errorEmptyName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
Text(AppLocalizations.of(context).people),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.height * 0.6,
|
||||||
|
child: Container(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: (_scenario.people.isEmpty)
|
||||||
|
? Text(AppLocalizations.of(context).addSomePeople)
|
||||||
|
: ExpansionPanelList(
|
||||||
|
expansionCallback: (panelIndex, isExpanded) {
|
||||||
|
_isOpen[panelIndex] = isExpanded;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
children: List<ExpansionPanel>.generate(
|
||||||
|
_scenario.people.length,
|
||||||
|
(index) => ExpansionPanel(
|
||||||
|
isExpanded: _isOpen[index],
|
||||||
|
headerBuilder: (context, isOpen) => Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context)
|
||||||
|
.size
|
||||||
|
.width *
|
||||||
|
0.45,
|
||||||
|
child: PlatformField(
|
||||||
|
labelText:
|
||||||
|
AppLocalizations.of(context)
|
||||||
|
.name,
|
||||||
|
controller:
|
||||||
|
_panelControllers[index],
|
||||||
|
onChanged: (input) {
|
||||||
|
_scenario.people[index].name =
|
||||||
|
input;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
validator: (input) {
|
||||||
|
if (input == null ||
|
||||||
|
input.isEmpty) {
|
||||||
|
return AppLocalizations.of(
|
||||||
|
context,
|
||||||
|
).errorEmptyName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_scenario.people.removeAt(index);
|
||||||
|
_panelControllers.removeRange(
|
||||||
|
index,
|
||||||
|
index + 2,
|
||||||
|
);
|
||||||
|
_isOpen.removeAt(index);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete_forever,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context)
|
||||||
|
.size
|
||||||
|
.width *
|
||||||
|
0.8,
|
||||||
|
child: PlatformField(
|
||||||
|
labelText:
|
||||||
|
AppLocalizations.of(context)
|
||||||
|
.bankAccount,
|
||||||
|
controller: _panelControllers[
|
||||||
|
index + 1],
|
||||||
|
onChanged: (input) {
|
||||||
|
_scenario.people[index]
|
||||||
|
.bankAccount =
|
||||||
|
(input.isEmpty)
|
||||||
|
? null
|
||||||
|
: input;
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
_scenario.people.add(
|
||||||
|
DebtPerson(id: _scenario.nextPersonId, name: ""),
|
||||||
|
);
|
||||||
|
_isOpen.add(false);
|
||||||
|
_panelControllers.addAll(
|
||||||
|
[TextEditingController(), TextEditingController()],
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (!_formKey.currentState!.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_scenario.people.isEmpty) {
|
||||||
|
showMessage(
|
||||||
|
AppLocalizations.of(context).noPersonError,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_scenario.name = _nameController.text;
|
||||||
|
if (widget.toEdit != null) {
|
||||||
|
// If editing, only replace
|
||||||
|
WalletManager.selectedWallet.debts[WalletManager
|
||||||
|
.selectedWallet.debts
|
||||||
|
.indexWhere((d) => d.id == widget.toEdit!.id)] =
|
||||||
|
_scenario;
|
||||||
|
} else {
|
||||||
|
// else add new
|
||||||
|
WalletManager.selectedWallet.debts.add(_scenario);
|
||||||
|
}
|
||||||
|
WalletManager.saveWallet(WalletManager.selectedWallet);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Text(AppLocalizations.of(context).save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,723 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
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';
|
|
||||||
import 'package:prasule/api/wallet_manager.dart';
|
|
||||||
import 'package:prasule/main.dart';
|
|
||||||
import 'package:prasule/pw/platformroute.dart';
|
|
||||||
import 'package:prasule/util/drawer.dart';
|
|
||||||
import 'package:prasule/util/graphs.dart';
|
|
||||||
import 'package:prasule/util/utils.dart';
|
|
||||||
import 'package:prasule/views/settings/settings.dart';
|
|
||||||
import 'package:prasule/views/setup.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'package:wheel_chooser/wheel_chooser.dart';
|
|
||||||
|
|
||||||
/// Shows data from a [Wallet] in graphs
|
|
||||||
class GraphView extends StatefulWidget {
|
|
||||||
/// Shows data from a [Wallet] in graphs
|
|
||||||
const GraphView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<GraphView> createState() => _GraphViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GraphViewState extends State<GraphView> {
|
|
||||||
var _selectedDate = DateTime.now();
|
|
||||||
Wallet? selectedWallet;
|
|
||||||
List<Wallet> wallets = [];
|
|
||||||
String? locale;
|
|
||||||
bool yearly = true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
locale ??= Localizations.localeOf(context).languageCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<double> generateChartData(EntryType type) {
|
|
||||||
final d = _selectedDate.add(const Duration(days: 31));
|
|
||||||
final data = List<double>.filled(
|
|
||||||
yearly ? 12 : DateTime(d.year, d.month, 0).day,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
if (selectedWallet == null) return [];
|
|
||||||
for (var i = 0; i < data.length; i++) {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
var sum = 0.0;
|
|
||||||
for (final e in entriesForRange) {
|
|
||||||
sum += e.data.amount;
|
|
||||||
}
|
|
||||||
data[i] = sum;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
final availableYears = <WheelChoice<int>>[];
|
|
||||||
|
|
||||||
void loadWallet() {
|
|
||||||
wallets = WalletManager.listWallets();
|
|
||||||
if (wallets.isEmpty && mounted) {
|
|
||||||
unawaited(
|
|
||||||
Navigator.of(context)
|
|
||||||
.pushReplacement(platformRoute((c) => const SetupView())),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedWallet = wallets.first;
|
|
||||||
availableYears.clear();
|
|
||||||
for (final entry in selectedWallet!.entries) {
|
|
||||||
if (!availableYears.any((element) => element.value == entry.date.year)) {
|
|
||||||
availableYears.add(
|
|
||||||
WheelChoice<int>(
|
|
||||||
value: entry.date.year,
|
|
||||||
title: entry.date.year.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
|
|
||||||
int? chartType;
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
loadWallet();
|
|
||||||
SharedPreferences.getInstance().then((s) {
|
|
||||||
chartType = s.getInt("yearlygraph") ?? 1;
|
|
||||||
logger.d(chartType);
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return DefaultTabController(
|
|
||||||
length: 2,
|
|
||||||
child: Scaffold(
|
|
||||||
floatingActionButton: Tooltip(
|
|
||||||
message: AppLocalizations.of(context).changeDate,
|
|
||||||
child: FloatingActionButton(
|
|
||||||
child: const Icon(Icons.calendar_month),
|
|
||||||
onPressed: () async {
|
|
||||||
var selectedYear = _selectedDate.year;
|
|
||||||
var selectedMonth = _selectedDate.month;
|
|
||||||
await showAdaptiveDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => AlertDialog.adaptive(
|
|
||||||
title: Text(
|
|
||||||
yearly
|
|
||||||
? AppLocalizations.of(context).selectYear
|
|
||||||
: AppLocalizations.of(context).selectMonth,
|
|
||||||
),
|
|
||||||
content: LimitedBox(
|
|
||||||
maxHeight: MediaQuery.of(context).size.width * 0.7,
|
|
||||||
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
|
||||||
child: Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 5,
|
|
||||||
children: [
|
|
||||||
if (!yearly)
|
|
||||||
SizedBox(
|
|
||||||
width: 120,
|
|
||||||
height: 100,
|
|
||||||
child: WheelChooser<int>.choices(
|
|
||||||
onChoiceChanged: (v) {
|
|
||||||
selectedMonth = v as int;
|
|
||||||
},
|
|
||||||
startPosition: _selectedDate.month - 1,
|
|
||||||
choices: List<WheelChoice<int>>.generate(
|
|
||||||
12,
|
|
||||||
(index) => WheelChoice(
|
|
||||||
value: index + 1,
|
|
||||||
title: DateFormat.MMMM(locale ?? "en").format(
|
|
||||||
DateTime(
|
|
||||||
_selectedDate.year,
|
|
||||||
index + 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 100,
|
|
||||||
width: 80,
|
|
||||||
child: WheelChooser<int>.choices(
|
|
||||||
startPosition: availableYears.indexWhere(
|
|
||||||
(element) => element.value == _selectedDate.year,
|
|
||||||
),
|
|
||||||
onChoiceChanged: (v) {
|
|
||||||
selectedYear = v as int;
|
|
||||||
},
|
|
||||||
choices: availableYears,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
_selectedDate = DateTime(selectedYear, selectedMonth);
|
|
||||||
Navigator.of(c).pop();
|
|
||||||
},
|
|
||||||
child: Text(AppLocalizations.of(context).ok),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
appBar: AppBar(
|
|
||||||
bottom: TabBar(
|
|
||||||
tabs: [
|
|
||||||
Tab(
|
|
||||||
child: Text(AppLocalizations.of(context).expenses),
|
|
||||||
),
|
|
||||||
Tab(
|
|
||||||
child: Text(AppLocalizations.of(context).incomePlural),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
title: DropdownButton<int>(
|
|
||||||
value: (selectedWallet == null)
|
|
||||||
? -1
|
|
||||||
: wallets.indexOf(
|
|
||||||
wallets.where((w) => w.name == selectedWallet!.name).first,
|
|
||||||
),
|
|
||||||
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 = 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(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.then((value) async {
|
|
||||||
selectedWallet =
|
|
||||||
WalletManager.loadWallet(selectedWallet!.name);
|
|
||||||
final s = await SharedPreferences.getInstance();
|
|
||||||
chartType = s.getInt("monthlygraph") ?? 2;
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
} else if (value == AppLocalizations.of(context).about) {
|
|
||||||
showAbout(context);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
drawer: makeDrawer(context, 2),
|
|
||||||
body: TabBarView(
|
|
||||||
children: [
|
|
||||||
// EXPENSE TAB
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Center(
|
|
||||||
child: (selectedWallet == null)
|
|
||||||
? const CircularProgressIndicator(
|
|
||||||
strokeWidth: 5,
|
|
||||||
)
|
|
||||||
: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 200,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).monthly,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Switch.adaptive(
|
|
||||||
value: yearly,
|
|
||||||
onChanged: (v) async {
|
|
||||||
yearly = v;
|
|
||||||
final s =
|
|
||||||
await SharedPreferences.getInstance();
|
|
||||||
chartType = yearly
|
|
||||||
? (s.getInt("yearlygraph") ?? 1)
|
|
||||||
: (s.getInt("monthlygraph") ?? 2);
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).yearly,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.5),
|
|
||||||
spreadRadius: 3,
|
|
||||||
blurRadius: 7,
|
|
||||||
offset: const Offset(
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
color: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.dark)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer
|
|
||||||
: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
yearly
|
|
||||||
? AppLocalizations.of(context)
|
|
||||||
.expensesPerYear(
|
|
||||||
_selectedDate.year,
|
|
||||||
)
|
|
||||||
: AppLocalizations.of(context)
|
|
||||||
.expensesPerMonth(
|
|
||||||
DateFormat.yMMMM(locale)
|
|
||||||
.format(_selectedDate),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 15,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width *
|
|
||||||
0.9,
|
|
||||||
height:
|
|
||||||
MediaQuery.of(context).size.height *
|
|
||||||
0.35,
|
|
||||||
child: (chartType == null)
|
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: (chartType == 1)
|
|
||||||
? ExpensesBarChart(
|
|
||||||
currency:
|
|
||||||
selectedWallet!.currency,
|
|
||||||
date: _selectedDate,
|
|
||||||
locale: locale ?? "en",
|
|
||||||
yearly: yearly,
|
|
||||||
expenseData:
|
|
||||||
generateChartData(
|
|
||||||
EntryType.expense,
|
|
||||||
),
|
|
||||||
incomeData: const [],
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.all(8),
|
|
||||||
child: ExpensesLineChart(
|
|
||||||
currency: selectedWallet!
|
|
||||||
.currency,
|
|
||||||
date: _selectedDate,
|
|
||||||
locale: locale ?? "en",
|
|
||||||
yearly: yearly,
|
|
||||||
expenseData:
|
|
||||||
generateChartData(
|
|
||||||
EntryType.expense,
|
|
||||||
),
|
|
||||||
incomeData: const [],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 25,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.5),
|
|
||||||
spreadRadius: 3,
|
|
||||||
blurRadius: 7,
|
|
||||||
offset: const Offset(
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
color: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.dark)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer
|
|
||||||
: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
width: MediaQuery.of(context).size.width * 0.95,
|
|
||||||
height: MediaQuery.of(context).size.height * 0.4,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
yearly
|
|
||||||
? AppLocalizations.of(context)
|
|
||||||
.expensesPerYearCategory(
|
|
||||||
_selectedDate.year,
|
|
||||||
)
|
|
||||||
: AppLocalizations.of(context)
|
|
||||||
.expensesPerMonthCategory(
|
|
||||||
DateFormat.yMMMM(locale)
|
|
||||||
.format(_selectedDate),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
child: CategoriesPieChart(
|
|
||||||
// TODO: better size adaptivity without overflow
|
|
||||||
locale: locale ?? "en",
|
|
||||||
symbol: selectedWallet!.currency.symbol,
|
|
||||||
entries: selectedWallet!.entries
|
|
||||||
.where(
|
|
||||||
(element) =>
|
|
||||||
((!yearly)
|
|
||||||
? element.date.month ==
|
|
||||||
_selectedDate
|
|
||||||
.month &&
|
|
||||||
element.date.year ==
|
|
||||||
_selectedDate.year
|
|
||||||
: element.date.year ==
|
|
||||||
_selectedDate.year) &&
|
|
||||||
element.type ==
|
|
||||||
EntryType.expense,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
categories: selectedWallet!.categories,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
), // Expense Tab END
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Center(
|
|
||||||
child: (selectedWallet == null)
|
|
||||||
? const CircularProgressIndicator(
|
|
||||||
strokeWidth: 5,
|
|
||||||
)
|
|
||||||
: SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width,
|
|
||||||
height: MediaQuery.of(context).size.height,
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: 200,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).monthly,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Switch.adaptive(
|
|
||||||
value: yearly,
|
|
||||||
onChanged: (v) async {
|
|
||||||
yearly = v;
|
|
||||||
final s =
|
|
||||||
await SharedPreferences.getInstance();
|
|
||||||
chartType = yearly
|
|
||||||
? (s.getInt("yearlygraph") ?? 1)
|
|
||||||
: (s.getInt("monthlygraph") ?? 2);
|
|
||||||
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).yearly,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.5),
|
|
||||||
spreadRadius: 3,
|
|
||||||
blurRadius: 7,
|
|
||||||
offset: const Offset(
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
color: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.dark)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer
|
|
||||||
: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
yearly
|
|
||||||
? AppLocalizations.of(context)
|
|
||||||
.incomePerYear(
|
|
||||||
_selectedDate.year,
|
|
||||||
)
|
|
||||||
: AppLocalizations.of(context)
|
|
||||||
.incomePerMonth(
|
|
||||||
DateFormat.yMMMM(locale)
|
|
||||||
.format(_selectedDate),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 15,
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: MediaQuery.of(context).size.width *
|
|
||||||
0.9,
|
|
||||||
height:
|
|
||||||
MediaQuery.of(context).size.height *
|
|
||||||
0.35,
|
|
||||||
child: (chartType == null)
|
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: (chartType == 1)
|
|
||||||
? ExpensesBarChart(
|
|
||||||
currency:
|
|
||||||
selectedWallet!.currency,
|
|
||||||
date: _selectedDate,
|
|
||||||
locale: locale ?? "en",
|
|
||||||
yearly: yearly,
|
|
||||||
expenseData: const [],
|
|
||||||
incomeData: generateChartData(
|
|
||||||
EntryType.income,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.all(8),
|
|
||||||
child: ExpensesLineChart(
|
|
||||||
currency: selectedWallet!
|
|
||||||
.currency,
|
|
||||||
date: _selectedDate,
|
|
||||||
locale: locale ?? "en",
|
|
||||||
yearly: yearly,
|
|
||||||
expenseData: const [],
|
|
||||||
incomeData:
|
|
||||||
generateChartData(
|
|
||||||
EntryType.income,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 25,
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
boxShadow: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.light)
|
|
||||||
? [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.5),
|
|
||||||
spreadRadius: 3,
|
|
||||||
blurRadius: 7,
|
|
||||||
offset: const Offset(
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: null,
|
|
||||||
color: (MediaQuery.of(context)
|
|
||||||
.platformBrightness ==
|
|
||||||
Brightness.dark)
|
|
||||||
? Theme.of(context)
|
|
||||||
.colorScheme
|
|
||||||
.secondaryContainer
|
|
||||||
: Theme.of(context).colorScheme.surface,
|
|
||||||
),
|
|
||||||
width: MediaQuery.of(context).size.width * 0.95,
|
|
||||||
height: MediaQuery.of(context).size.height * 0.4,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
yearly
|
|
||||||
? AppLocalizations.of(context)
|
|
||||||
.incomePerYearCategory(
|
|
||||||
_selectedDate.year,
|
|
||||||
)
|
|
||||||
: AppLocalizations.of(context)
|
|
||||||
.incomePerMonthCategory(
|
|
||||||
DateFormat.yMMMM(locale)
|
|
||||||
.format(_selectedDate),
|
|
||||||
),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
child: CategoriesPieChart(
|
|
||||||
locale: locale ?? "en",
|
|
||||||
symbol: selectedWallet!.currency.symbol,
|
|
||||||
entries: selectedWallet!.entries
|
|
||||||
.where(
|
|
||||||
(element) =>
|
|
||||||
((!yearly)
|
|
||||||
? element.date.month ==
|
|
||||||
_selectedDate
|
|
||||||
.month &&
|
|
||||||
element.date.year ==
|
|
||||||
_selectedDate.year
|
|
||||||
: element.date.year ==
|
|
||||||
_selectedDate.year) &&
|
|
||||||
element.type ==
|
|
||||||
EntryType.income,
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
categories: selectedWallet!.categories,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
), // Income Tab END
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
688
lib/views/graphs/graph_view.dart
Normal file
688
lib/views/graphs/graph_view.dart
Normal file
|
@ -0,0 +1,688 @@
|
||||||
|
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
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';
|
||||||
|
import 'package:prasule/api/wallet_manager.dart';
|
||||||
|
import 'package:prasule/main.dart';
|
||||||
|
import 'package:prasule/pw/platformroute.dart';
|
||||||
|
import 'package:prasule/util/drawer.dart';
|
||||||
|
import 'package:prasule/util/graphs.dart';
|
||||||
|
import 'package:prasule/util/utils.dart';
|
||||||
|
import 'package:prasule/views/settings/settings.dart';
|
||||||
|
import 'package:prasule/views/setup.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:wheel_chooser/wheel_chooser.dart';
|
||||||
|
|
||||||
|
/// Shows data from a [Wallet] in graphs
|
||||||
|
class GraphView extends StatefulWidget {
|
||||||
|
/// Shows data from a [Wallet] in graphs
|
||||||
|
const GraphView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<GraphView> createState() => _GraphViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GraphViewState extends State<GraphView> {
|
||||||
|
var _selectedDate = DateTime.now();
|
||||||
|
List<Wallet> wallets = [];
|
||||||
|
String? locale;
|
||||||
|
bool yearly = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
locale ??= Localizations.localeOf(context).languageCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<double> generateChartData(EntryType type) {
|
||||||
|
final d = _selectedDate.add(const Duration(days: 31));
|
||||||
|
final data = List<double>.filled(
|
||||||
|
yearly ? 12 : DateTime(d.year, d.month, 0).day,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
final entriesForRange = WalletManager.selectedWallet.entries.where(
|
||||||
|
(element) =>
|
||||||
|
((!yearly)
|
||||||
|
? element.date.month == _selectedDate.month &&
|
||||||
|
element.date.year == _selectedDate.year &&
|
||||||
|
element.date.day == i + 1
|
||||||
|
: element.date.month == i + 1 &&
|
||||||
|
element.date.year == _selectedDate.year) &&
|
||||||
|
element.type == type,
|
||||||
|
);
|
||||||
|
var sum = 0.0;
|
||||||
|
for (final e in entriesForRange) {
|
||||||
|
sum += e.data.amount;
|
||||||
|
}
|
||||||
|
data[i] = sum;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
final availableYears = <WheelChoice<int>>[];
|
||||||
|
|
||||||
|
void loadWallet() {
|
||||||
|
wallets = WalletManager.listWallets();
|
||||||
|
if (wallets.isEmpty && mounted) {
|
||||||
|
unawaited(
|
||||||
|
Navigator.of(context)
|
||||||
|
.pushReplacement(platformRoute((c) => const SetupView())),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WalletManager.selectedWallet = wallets.first;
|
||||||
|
availableYears.clear();
|
||||||
|
for (final entry in WalletManager.selectedWallet.entries) {
|
||||||
|
if (!availableYears.any((element) => element.value == entry.date.year)) {
|
||||||
|
availableYears.add(
|
||||||
|
WheelChoice<int>(
|
||||||
|
value: entry.date.year,
|
||||||
|
title: entry.date.year.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
int? chartType;
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
loadWallet();
|
||||||
|
SharedPreferences.getInstance().then((s) {
|
||||||
|
chartType = s.getInt("yearlygraph") ?? 1;
|
||||||
|
logger.d(chartType);
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
child: Scaffold(
|
||||||
|
floatingActionButton: Tooltip(
|
||||||
|
message: AppLocalizations.of(context).changeDate,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.calendar_month),
|
||||||
|
onPressed: () async {
|
||||||
|
var selectedYear = _selectedDate.year;
|
||||||
|
var selectedMonth = _selectedDate.month;
|
||||||
|
await showAdaptiveDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => AlertDialog.adaptive(
|
||||||
|
title: Text(
|
||||||
|
yearly
|
||||||
|
? AppLocalizations.of(context).selectYear
|
||||||
|
: AppLocalizations.of(context).selectMonth,
|
||||||
|
),
|
||||||
|
content: LimitedBox(
|
||||||
|
maxHeight: MediaQuery.of(context).size.width * 0.7,
|
||||||
|
maxWidth: MediaQuery.of(context).size.width * 0.8,
|
||||||
|
child: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 5,
|
||||||
|
children: [
|
||||||
|
if (!yearly)
|
||||||
|
SizedBox(
|
||||||
|
width: 120,
|
||||||
|
height: 100,
|
||||||
|
child: WheelChooser<int>.choices(
|
||||||
|
onChoiceChanged: (v) {
|
||||||
|
selectedMonth = v as int;
|
||||||
|
},
|
||||||
|
startPosition: _selectedDate.month - 1,
|
||||||
|
choices: List<WheelChoice<int>>.generate(
|
||||||
|
12,
|
||||||
|
(index) => WheelChoice(
|
||||||
|
value: index + 1,
|
||||||
|
title: DateFormat.MMMM(locale ?? "en").format(
|
||||||
|
DateTime(
|
||||||
|
_selectedDate.year,
|
||||||
|
index + 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 100,
|
||||||
|
width: 80,
|
||||||
|
child: WheelChooser<int>.choices(
|
||||||
|
startPosition: availableYears.indexWhere(
|
||||||
|
(element) => element.value == _selectedDate.year,
|
||||||
|
),
|
||||||
|
onChoiceChanged: (v) {
|
||||||
|
selectedYear = v as int;
|
||||||
|
},
|
||||||
|
choices: availableYears,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_selectedDate = DateTime(selectedYear, selectedMonth);
|
||||||
|
Navigator.of(c).pop();
|
||||||
|
},
|
||||||
|
child: Text(AppLocalizations.of(context).ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
appBar: AppBar(
|
||||||
|
bottom: TabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(
|
||||||
|
child: Text(AppLocalizations.of(context).expenses),
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
child: Text(AppLocalizations.of(context).incomePlural),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
title: DropdownButton<int>(
|
||||||
|
value: wallets.indexOf(
|
||||||
|
wallets
|
||||||
|
.where((w) => w.name == WalletManager.selectedWallet.name)
|
||||||
|
.first,
|
||||||
|
),
|
||||||
|
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 = WalletManager.listWallets();
|
||||||
|
logger.i(wallets.length);
|
||||||
|
WalletManager.selectedWallet = wallets.last;
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WalletManager.selectedWallet = wallets[v];
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
PopupMenuButton(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
AppLocalizations.of(context).settings,
|
||||||
|
AppLocalizations.of(context).about,
|
||||||
|
].map((e) => PopupMenuItem(value: e, child: Text(e))).toList(),
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == AppLocalizations.of(context).settings) {
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(
|
||||||
|
platformRoute(
|
||||||
|
(context) => const SettingsView(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then((value) async {
|
||||||
|
WalletManager.selectedWallet = WalletManager.loadWallet(
|
||||||
|
WalletManager.selectedWallet.name);
|
||||||
|
final s = await SharedPreferences.getInstance();
|
||||||
|
chartType = s.getInt("monthlygraph") ?? 2;
|
||||||
|
setState(() {});
|
||||||
|
});
|
||||||
|
} else if (value == AppLocalizations.of(context).about) {
|
||||||
|
showAbout(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
drawer: makeDrawer(context, Pages.graphs),
|
||||||
|
body: TabBarView(
|
||||||
|
children: [
|
||||||
|
// EXPENSE TAB
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).monthly,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: yearly,
|
||||||
|
onChanged: (v) async {
|
||||||
|
yearly = v;
|
||||||
|
final s = await SharedPreferences.getInstance();
|
||||||
|
chartType = yearly
|
||||||
|
? (s.getInt("yearlygraph") ?? 1)
|
||||||
|
: (s.getInt("monthlygraph") ?? 2);
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).yearly,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow:
|
||||||
|
(MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
color: (MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark)
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
yearly
|
||||||
|
? AppLocalizations.of(context)
|
||||||
|
.expensesPerYear(
|
||||||
|
_selectedDate.year,
|
||||||
|
)
|
||||||
|
: AppLocalizations.of(context)
|
||||||
|
.expensesPerMonth(
|
||||||
|
DateFormat.yMMMM(locale)
|
||||||
|
.format(_selectedDate),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.35,
|
||||||
|
child: (chartType == null)
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: (chartType == 1)
|
||||||
|
? ExpensesBarChart(
|
||||||
|
currency: WalletManager
|
||||||
|
.selectedWallet.currency,
|
||||||
|
date: _selectedDate,
|
||||||
|
locale: locale ?? "en",
|
||||||
|
yearly: yearly,
|
||||||
|
expenseData: generateChartData(
|
||||||
|
EntryType.expense,
|
||||||
|
),
|
||||||
|
incomeData: const [],
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ExpensesLineChart(
|
||||||
|
currency: WalletManager
|
||||||
|
.selectedWallet.currency,
|
||||||
|
date: _selectedDate,
|
||||||
|
locale: locale ?? "en",
|
||||||
|
yearly: yearly,
|
||||||
|
expenseData: generateChartData(
|
||||||
|
EntryType.expense,
|
||||||
|
),
|
||||||
|
incomeData: const [],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow:
|
||||||
|
(MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
color: (MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark)
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
width: MediaQuery.of(context).size.width * 0.95,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.4,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
yearly
|
||||||
|
? AppLocalizations.of(context)
|
||||||
|
.expensesPerYearCategory(
|
||||||
|
_selectedDate.year,
|
||||||
|
)
|
||||||
|
: AppLocalizations.of(context)
|
||||||
|
.expensesPerMonthCategory(
|
||||||
|
DateFormat.yMMMM(locale)
|
||||||
|
.format(_selectedDate),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: CategoriesPieChart(
|
||||||
|
// TODO: better size adaptivity without overflow
|
||||||
|
locale: locale ?? "en",
|
||||||
|
symbol: WalletManager
|
||||||
|
.selectedWallet.currency.symbol,
|
||||||
|
entries: WalletManager.selectedWallet.entries
|
||||||
|
.where(
|
||||||
|
(element) =>
|
||||||
|
((!yearly)
|
||||||
|
? element.date.month ==
|
||||||
|
_selectedDate.month &&
|
||||||
|
element.date.year ==
|
||||||
|
_selectedDate.year
|
||||||
|
: element.date.year ==
|
||||||
|
_selectedDate.year) &&
|
||||||
|
element.type == EntryType.expense,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
categories:
|
||||||
|
WalletManager.selectedWallet.categories,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // Expense Tab END
|
||||||
|
SingleChildScrollView(
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: MediaQuery.of(context).size.height,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).monthly,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch.adaptive(
|
||||||
|
value: yearly,
|
||||||
|
onChanged: (v) async {
|
||||||
|
yearly = v;
|
||||||
|
final s = await SharedPreferences.getInstance();
|
||||||
|
chartType = yearly
|
||||||
|
? (s.getInt("yearlygraph") ?? 1)
|
||||||
|
: (s.getInt("monthlygraph") ?? 2);
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).yearly,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow:
|
||||||
|
(MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
color: (MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark)
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
yearly
|
||||||
|
? AppLocalizations.of(context)
|
||||||
|
.incomePerYear(
|
||||||
|
_selectedDate.year,
|
||||||
|
)
|
||||||
|
: AppLocalizations.of(context)
|
||||||
|
.incomePerMonth(
|
||||||
|
DateFormat.yMMMM(locale)
|
||||||
|
.format(_selectedDate),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 15,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: MediaQuery.of(context).size.width * 0.9,
|
||||||
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.35,
|
||||||
|
child: (chartType == null)
|
||||||
|
? const CircularProgressIndicator()
|
||||||
|
: (chartType == 1)
|
||||||
|
? ExpensesBarChart(
|
||||||
|
currency: WalletManager
|
||||||
|
.selectedWallet.currency,
|
||||||
|
date: _selectedDate,
|
||||||
|
locale: locale ?? "en",
|
||||||
|
yearly: yearly,
|
||||||
|
expenseData: const [],
|
||||||
|
incomeData: generateChartData(
|
||||||
|
EntryType.income,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: ExpensesLineChart(
|
||||||
|
currency: WalletManager
|
||||||
|
.selectedWallet.currency,
|
||||||
|
date: _selectedDate,
|
||||||
|
locale: locale ?? "en",
|
||||||
|
yearly: yearly,
|
||||||
|
expenseData: const [],
|
||||||
|
incomeData: generateChartData(
|
||||||
|
EntryType.income,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 25,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
boxShadow:
|
||||||
|
(MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.light)
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.5),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
color: (MediaQuery.of(context).platformBrightness ==
|
||||||
|
Brightness.dark)
|
||||||
|
? Theme.of(context).colorScheme.secondaryContainer
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
|
),
|
||||||
|
width: MediaQuery.of(context).size.width * 0.95,
|
||||||
|
height: MediaQuery.of(context).size.height * 0.4,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
yearly
|
||||||
|
? AppLocalizations.of(context)
|
||||||
|
.incomePerYearCategory(
|
||||||
|
_selectedDate.year,
|
||||||
|
)
|
||||||
|
: AppLocalizations.of(context)
|
||||||
|
.incomePerMonthCategory(
|
||||||
|
DateFormat.yMMMM(locale)
|
||||||
|
.format(_selectedDate),
|
||||||
|
),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
child: CategoriesPieChart(
|
||||||
|
locale: locale ?? "en",
|
||||||
|
symbol: WalletManager
|
||||||
|
.selectedWallet.currency.symbol,
|
||||||
|
entries: WalletManager.selectedWallet.entries
|
||||||
|
.where(
|
||||||
|
(element) =>
|
||||||
|
((!yearly)
|
||||||
|
? element.date.month ==
|
||||||
|
_selectedDate.month &&
|
||||||
|
element.date.year ==
|
||||||
|
_selectedDate.year
|
||||||
|
: element.date.year ==
|
||||||
|
_selectedDate.year) &&
|
||||||
|
element.type == EntryType.income,
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
categories:
|
||||||
|
WalletManager.selectedWallet.categories,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
), // Income Tab END
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,7 +29,7 @@ import 'package:prasule/util/drawer.dart';
|
||||||
import 'package:prasule/util/sorting.dart';
|
import 'package:prasule/util/sorting.dart';
|
||||||
import 'package:prasule/util/text_color.dart';
|
import 'package:prasule/util/text_color.dart';
|
||||||
import 'package:prasule/util/utils.dart';
|
import 'package:prasule/util/utils.dart';
|
||||||
import 'package:prasule/views/create_entry.dart';
|
import 'package:prasule/views/entry/create_entry.dart';
|
||||||
import 'package:prasule/views/settings/settings.dart';
|
import 'package:prasule/views/settings/settings.dart';
|
||||||
import 'package:prasule/views/settings/tessdata_list.dart';
|
import 'package:prasule/views/settings/tessdata_list.dart';
|
||||||
import 'package:prasule/views/setup.dart';
|
import 'package:prasule/views/setup.dart';
|
||||||
|
@ -94,8 +94,10 @@ class _HomeViewState extends State<HomeView> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
drawer: makeDrawer(context, 1),
|
drawer: makeDrawer(context, Pages.home),
|
||||||
floatingActionButton: SpeedDial(
|
floatingActionButton: SpeedDial(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
),
|
),
|
||||||
|
|
|
@ -31,6 +31,7 @@ class _InitializationScreenState extends State<InitializationScreen> {
|
||||||
.pushReplacement(platformRoute((c) => const SetupView()));
|
.pushReplacement(platformRoute((c) => const SetupView()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
WalletManager.selectedWallet = wallets.first;
|
||||||
Navigator.of(context)
|
Navigator.of(context)
|
||||||
.pushReplacement(platformRoute((c) => const HomeView()));
|
.pushReplacement(platformRoute((c) => const HomeView()));
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ import 'package:prasule/pw/platformroute.dart';
|
||||||
import 'package:prasule/util/drawer.dart';
|
import 'package:prasule/util/drawer.dart';
|
||||||
import 'package:prasule/util/text_color.dart';
|
import 'package:prasule/util/text_color.dart';
|
||||||
import 'package:prasule/util/utils.dart';
|
import 'package:prasule/util/utils.dart';
|
||||||
import 'package:prasule/views/create_recur_entry.dart';
|
import 'package:prasule/views/recurring/create_recur_entry.dart';
|
||||||
import 'package:prasule/views/settings/settings.dart';
|
import 'package:prasule/views/settings/settings.dart';
|
||||||
import 'package:prasule/views/setup.dart';
|
import 'package:prasule/views/setup.dart';
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
drawer: makeDrawer(context, 3),
|
drawer: makeDrawer(context, Pages.recurringEntries),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: DropdownButton<int>(
|
title: DropdownButton<int>(
|
||||||
value: (selectedWallet == null)
|
value: (selectedWallet == null)
|
|
@ -167,6 +167,9 @@ class _SetupViewState extends State<SetupView> {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WalletManager.selectedWallet = wallet;
|
||||||
|
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
unawaited(
|
unawaited(
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
|
|
|
@ -23,7 +23,7 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_file_dialog: ^3.0.2
|
flutter_file_dialog: ^3.0.2
|
||||||
flutter_iconpicker: <3.4.0
|
flutter_iconpicker: <3.4.7
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_slidable: ^3.0.0
|
flutter_slidable: ^3.0.0
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
sonar.projectKey=prasule
|
sonar.projectKey=prasule
|
||||||
sonar.sources=lib,pubspec.yaml
|
sonar.sources=lib,pubspec.yaml
|
||||||
sonar.tests=test
|
sonar.tests=integration_test,test_driver
|
||||||
sonar.dart.analyzer.mode=MANUAL
|
sonar.dart.analyzer.mode=FLUTTER
|
||||||
sonar.dart.analyzer.options.override=true
|
sonar.dart.analyzer.options.override=true
|
||||||
sonar.host.url=https://sq.mnau.xyz
|
sonar.host.url=https://sq.mnau.xyz
|
Loading…
Reference in a new issue