Compare commits
64 commits
Author | SHA1 | Date | |
---|---|---|---|
513a276031 | |||
|
2d50509163 | ||
ef52caa836 | |||
300f359070 | |||
6530b58c19 | |||
2339739ff8 | |||
5f99e35709 | |||
fd04bc1dc4 | |||
1b7eac52ee | |||
2cc628ce89 | |||
68a89444dd | |||
4cd4913d35 | |||
8665db6c0f | |||
6a5ddedffd | |||
dbcc7ad972 | |||
d7dbf4fff0 | |||
01b9decfdc | |||
5733f52572 | |||
66848fdc2d | |||
031d9c2ea3 | |||
537b520288 | |||
4808a142aa | |||
1c2ef630c3 | |||
ecc14affe8 | |||
cb5dbeffcf | |||
1d1391d0df | |||
32432d760c | |||
ba3b963d84 | |||
e1d3a54ebf | |||
8e46b936a8 | |||
d93eac4f04 | |||
94eb563217 | |||
cddb58c118 | |||
62e0c2ba3d | |||
07908797d5 | |||
1855ed4e6b | |||
09d5bbb953 | |||
|
80f2ae9f7f | ||
|
481716a3b2 | ||
|
8ff63c9492 | ||
|
1faa5e2f78 | ||
|
5cbb9a12dc | ||
|
58dd06f39f | ||
|
d832080a6d | ||
|
a90a39feff | ||
41d45e96fb | |||
|
3ff35fd427 | ||
|
f7b6219d31 | ||
|
638b6eef67 | ||
|
92e185dcb1 | ||
|
85fefda4d8 | ||
|
7c8535b99d | ||
|
15d2590a8e | ||
|
bd17e12e1b | ||
|
af04accabf | ||
|
c7e40b12b7 | ||
|
4a3f097b02 | ||
|
e5e803057a | ||
|
a6872408fa | ||
|
4d7144267e | ||
|
aeda571f35 | ||
|
776e9b056a | ||
|
7dbf4c969a | ||
|
76da494804 |
68 changed files with 2236 additions and 1158 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 1751123cde4ffad08ae27bdee4f8ddebd033fe76
|
||||
Subproject commit 2feea7a4071e25c1e3aac9c17016531bc4442f2a
|
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
|||
# 2.0.0
|
||||
- Upgrade dependencies
|
||||
- Use less `await`s in WalletManager class
|
||||
- Added debt management
|
||||
- Changed android app ID
|
||||
|
||||
# 1.1.1
|
||||
- Removed deprecated code
|
||||
- Updated dependencies
|
||||
- Fix wrong graph type upon opening graph view
|
||||
- Fix text fields behaving unexpectedly
|
||||
- Fix `Add new wallet` showing in the app bar when returning from another view
|
||||
- Fix new wallet name being rewritten to the default placeholder
|
||||
|
||||
# 1.1.0
|
||||
- Fix indicators for Pie Chart
|
||||
- Fix entries not showing up immediately after creation
|
||||
|
|
|
@ -1,5 +1,95 @@
|
|||
# Localization
|
||||
# Contributing to Prašule
|
||||
Hello! I'm glad you want to contribute to Prašule. It is currently a one-man project, so I alwaýs appreciate more help.
|
||||
|
||||
Below you can see all the different ways you can help the project. Make sure to read the sections carefully, if there's something you don't understand or feel like is missing, don't be afraid to create an issue or send me an e-mail. :)
|
||||
|
||||
## Before contributing
|
||||
Prašule is an open-source project, all code and assets are licensed under the GNU AGPL version 3 (see [LICENSE.md](https://git.mnau.xyz/hernik/prasule/src/commit/cff9f6c8c72156d7c9ee761d2e6c15b37218c166/LICENSE.md) for more details), unless otherwise stated. Localizations are published under [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.en).
|
||||
|
||||
By contributing to this project you agree that you are the author of your contributions (or have the rights to publish them to this project) and that they will be published under the license of the contribution, depending if it's localization or code.
|
||||
|
||||
## Localization
|
||||
|
||||
[![Translation status](https://hosted.weblate.org/widget/prasule/287x66-grey.png)](https://hosted.weblate.org/engage/prasule/)
|
||||
|
||||
You can help with localization through Weblate, join the translation project on https://hosted.weblate.org/engage/prasule/
|
||||
You can help with localization through Weblate, join the translation project on https://hosted.weblate.org/engage/prasule/
|
||||
|
||||
## Filing bug reports
|
||||
Probably one of the easiest ways to help the project is to use the app and find bugs and other problems. Before submitting a issue, check the list of [open and closed issues](https://git.mnau.xyz/hernik/prasule/issues) so we don't have to deal with duplicates.
|
||||
|
||||
If you ever find something,
|
||||
file a new issue using the relevant template [here](https://git.mnau.xyz/hernik/prasule/issues/new/choose), make sure to fill up as much information as you can.
|
||||
|
||||
Template preview:
|
||||
```md
|
||||
### Operating System + Version
|
||||
<!-- for example "Android 13" -->
|
||||
|
||||
### App version
|
||||
<!--
|
||||
Can be found in the app by clicking
|
||||
the three dots > 'About' or in app info in your device's setting
|
||||
-->
|
||||
|
||||
### What was expected to happen
|
||||
<!-- Enter what you thought SHOULD happen when the bug occured -->
|
||||
|
||||
### What actually happened
|
||||
<!-- Here describe what ACTUALLY happened -->
|
||||
|
||||
### Relevant logs
|
||||
```
|
||||
Paste your logs here
|
||||
|
||||
Make sure it is a codebloc
|
||||
```
|
||||
|
||||
### Steps to reproduce
|
||||
<!--
|
||||
Enter the exact steps that you made when you encountered the bug,
|
||||
so we can reproduce it
|
||||
-->
|
||||
|
||||
- [] I have checked the [list of issues](https://git.mnau.xyz/hernik/prasule/issues) and I'm sure this is not a duplicate
|
||||
<!-- This is a checkbox, either check it after creating the issue or change [] to [X] -->
|
||||
```
|
||||
|
||||
## Suggesting features
|
||||
Features can be suggested using the same issue tracker, make sure to choose the feature request template when creating a [new issue](https://git.mnau.xyz/hernik/prasule/issues/new/choose).
|
||||
|
||||
Template preview:
|
||||
```md
|
||||
- [] I have checked the [list of issues](https://git.mnau.xyz/hernik/prasule/issues) and I'm sure this is not a duplicate
|
||||
<!-- This is a checkbox, either check it after creating the issue or change [] to [X] -->
|
||||
|
||||
### Is your feature request related to a bug or an issue? If yes, describe it or link it
|
||||
<!-- You can link issues by simply writing # followed by the ID of the issue or PR, for example #12 -->
|
||||
|
||||
### Describe your request, how you'd like it to be implemented etc.
|
||||
<!-- This is the space for your requests to be made -->
|
||||
```
|
||||
|
||||
## Code contributions
|
||||
Code contributions can either be bug fixes related to open issues or new features. First let's get you ready with a dev environment.
|
||||
|
||||
### Running the app locally
|
||||
To have everyone use the same Flutter version, this project uses [Flutter Wrapper](https://github.com/passsy/flutter_wrapper) for version management. The version is kept using git submodules, so when you're cloning the repo, make sure to add a `--recurse-submodules` flag to your clone command.
|
||||
|
||||
Because the wrapper operates using a shell script, you'll have to use WSL on Windows or download the specific Flutter version. We use the `beta` update channel. After the project has cloned with the submodules, use `flutterw` (not `flutter`) for every Flutter command.
|
||||
|
||||
Make sure to set up your device (debug mode or VM for Android, simulator or physical device for iOS) and run the development build using `./flutterw run`. The app should now install and run on your device.
|
||||
|
||||
### Code conventions
|
||||
This project uses the [very_good_analysis](https://pub.dev/packages/very_good_analysis) lint package (with minor edits seen in `analysis_options.yaml`). If you don't have an IDE that shows lints, you can check for lint errors using the `./flutterw analyze` command. Certain issues have automated fixes, but don't rely on them, they can make mistakes sometimes.
|
||||
|
||||
Certain lint issues, like `lines_longer_than_80_chars`, can be ignored in certain scenarios, but try to have the code issue-free.
|
||||
|
||||
### Submitting new code to the repo
|
||||
First of all, fork the repo to your own profile and clone it (if you've already cloned it, change the remote URL: `git remote set-url origin https://git.mnau.xyz/YOUR_USERNAME/prasule.git` and pull any changes).
|
||||
|
||||
The `main` branch contains only stable code ready for shipping, all development occurs on the `dev` branch, so create a new branch **from the `dev` branch**. There are no branch naming conventions, but if it's a new feature, it could be `feat-featurename`, for bug fixes `fix-issuename`.
|
||||
|
||||
Check out your branch in your workspace and start coding. When you're done and want to commit your changes, make sure the commit message adheres the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
|
||||
When all your changes are ready to be pushed to the main repo, submit a new [pull request](https://forgejo.org/docs/latest/user/pull-requests-and-git-flow/) **into the `dev` branch**. The PR will be reviewed by a maintainer, make sure to check back from time to time for any additional requests.
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
# 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
|
||||
|
||||
- [Apple Testflight](https://testflight.apple.com/join/C22pcnPc)
|
||||
- [Google Play beta testing](https://play.google.com/store/apps/details?id=cafe.caras.prasule)
|
||||
|
||||
## License
|
||||
``` Prašule - simple, private & open-source expense tracker
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:very_good_analysis/analysis_options.yaml
|
||||
|
||||
analyzer:
|
||||
exclude: [/**/*.g.dart]
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
|
|
|
@ -48,11 +48,11 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
// 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.
|
||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
|
3
assets/icon/dynamic_foreground.png.license
Normal file
3
assets/icon/dynamic_foreground.png.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
3
assets/icon/full_ico.png.license
Normal file
3
assets/icon/full_ico.png.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
3
assets/tessdata/eng.traineddata.license
Normal file
3
assets/tessdata/eng.traineddata.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Tesseract-OCR contributors
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
3
assets/tessdata_config.json.license
Normal file
3
assets/tessdata_config.json.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
@ -1,7 +1,12 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
|
@ -14,15 +19,18 @@ import 'package:prasule/pw/platformfield.dart';
|
|||
void main() {
|
||||
final logger = Logger();
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() async {
|
||||
WalletManager.walletPath =
|
||||
"${(await getApplicationDocumentsDirectory()).path}/wallets";
|
||||
});
|
||||
group("Test classes and API", () {
|
||||
test("Test wallet operations", () async {
|
||||
expect(
|
||||
(await WalletManager.listWallets()).length,
|
||||
WalletManager.listWallets().length,
|
||||
equals(0),
|
||||
); // check that there are no other wallets
|
||||
await WalletManager.saveWallet(Wallet.empty);
|
||||
var w = (await WalletManager.listWallets()).firstOrNull;
|
||||
WalletManager.saveWallet(Wallet.empty);
|
||||
var w = WalletManager.listWallets().firstOrNull;
|
||||
expect(w, isNotNull); // check that the wallet was successfully saved
|
||||
expect(w!.categories.length, equals(1));
|
||||
w.categories.add(
|
||||
|
@ -43,8 +51,8 @@ void main() {
|
|||
id: w.nextId,
|
||||
),
|
||||
); // create test entry
|
||||
await WalletManager.saveWallet(w); // save again
|
||||
w = await WalletManager.loadWallet(w.name); // try loading manually
|
||||
WalletManager.saveWallet(w); // save again
|
||||
w = WalletManager.loadWallet(w.name); // try loading manually
|
||||
final e = w.entries.where((element) => element.id == testId).firstOrNull;
|
||||
expect(
|
||||
e,
|
||||
|
@ -55,9 +63,9 @@ void main() {
|
|||
equals(1),
|
||||
); // check that the category exists too
|
||||
|
||||
await WalletManager.deleteWallet(w);
|
||||
WalletManager.deleteWallet(w);
|
||||
expect(
|
||||
(await WalletManager.listWallets()).length,
|
||||
WalletManager.listWallets().length,
|
||||
equals(0),
|
||||
);
|
||||
});
|
||||
|
@ -66,7 +74,7 @@ void main() {
|
|||
group("Test app functionality:", () {
|
||||
testWidgets('First-time setup', (WidgetTester tester) async {
|
||||
// Delete all data
|
||||
await WalletManager.deleteAllData();
|
||||
WalletManager.deleteAllData();
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(
|
||||
const MyApp(
|
||||
|
@ -113,13 +121,12 @@ void main() {
|
|||
|
||||
testWidgets('Test rendering of entries', (WidgetTester tester) async {
|
||||
// Delete all data
|
||||
await WalletManager.deleteAllData();
|
||||
expect((await WalletManager.listWallets()).length, equals(0));
|
||||
WalletManager.deleteAllData();
|
||||
expect(WalletManager.listWallets().length, equals(0));
|
||||
|
||||
// Create test data
|
||||
final w = Wallet.empty;
|
||||
await w.createTestEntries();
|
||||
expect((await WalletManager.listWallets()).length, equals(1));
|
||||
Wallet.empty.createTestEntries();
|
||||
expect(WalletManager.listWallets().length, equals(1));
|
||||
|
||||
// Build our app and trigger a frame.
|
||||
await tester.pumpWidget(
|
||||
|
|
|
@ -465,7 +465,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
ARCHS = x86_64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
|
@ -647,7 +647,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
ARCHS = x86_64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
|
@ -672,7 +672,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ARCHS = "$(ARCHS_STANDARD)";
|
||||
ARCHS = x86_64;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'category.g.dart';
|
||||
|
@ -14,25 +18,43 @@ class WalletCategory {
|
|||
required this.color,
|
||||
});
|
||||
|
||||
/// Connects the generated fromJson method
|
||||
/// Generates a class instance from a Map
|
||||
factory WalletCategory.fromJson(Map<String, dynamic> json) =>
|
||||
_$WalletCategoryFromJson(json);
|
||||
|
||||
/// Default [WalletCategory] instance for json_serializable
|
||||
factory WalletCategory.unknown() => WalletCategory(
|
||||
name: "Unknown",
|
||||
id: -1,
|
||||
icon: Icons.question_mark,
|
||||
color: Colors.green,
|
||||
);
|
||||
|
||||
/// User-defined name
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// Unique identificator of the category
|
||||
@JsonKey(required: true, disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
/// Selected Icon for the category
|
||||
@JsonKey(fromJson: _iconDataFromJson, toJson: _iconDataToJson)
|
||||
@JsonKey(
|
||||
fromJson: _iconDataFromJson,
|
||||
toJson: _iconDataToJson,
|
||||
defaultValue: _defaultIcon,
|
||||
)
|
||||
IconData icon;
|
||||
|
||||
/// The color that will be displayed with entry
|
||||
@JsonKey(fromJson: _colorFromJson, toJson: _colorToJson)
|
||||
@JsonKey(
|
||||
fromJson: _colorFromJson,
|
||||
toJson: _colorToJson,
|
||||
defaultValue: _defaultColor,
|
||||
)
|
||||
Color color;
|
||||
|
||||
/// Connects the generated toJson method
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$WalletCategoryToJson(this);
|
||||
|
||||
@override
|
||||
|
@ -58,3 +80,6 @@ enum EntryType {
|
|||
/// Income
|
||||
income
|
||||
}
|
||||
|
||||
IconData _defaultIcon() => Icons.question_mark;
|
||||
Color _defaultColor() => Colors.green;
|
||||
|
|
|
@ -6,13 +6,23 @@ part of 'category.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) =>
|
||||
WalletCategory(
|
||||
name: json['name'] as String,
|
||||
id: json['id'] as int,
|
||||
icon: _iconDataFromJson(json['icon'] as Map<String, dynamic>),
|
||||
color: _colorFromJson(json['color'] as int),
|
||||
);
|
||||
WalletCategory _$WalletCategoryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return WalletCategory(
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
id: (json['id'] as num).toInt(),
|
||||
icon: json['icon'] == null
|
||||
? _defaultIcon()
|
||||
: _iconDataFromJson(json['icon'] as Map<String, dynamic>),
|
||||
color: json['color'] == null
|
||||
? _defaultColor()
|
||||
: _colorFromJson((json['color'] as num).toInt()),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WalletCategoryToJson(WalletCategory instance) =>
|
||||
<String, dynamic>{
|
||||
|
|
44
lib/api/debt_entry.dart
Normal file
44
lib/api/debt_entry.dart
Normal file
|
@ -0,0 +1,44 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/debt_person.dart';
|
||||
part 'debt_entry.g.dart';
|
||||
|
||||
/// Single transaction
|
||||
///
|
||||
/// The debt will be split between people who are not in [whoPayed]
|
||||
@JsonSerializable()
|
||||
class DebtEntry {
|
||||
/// Single transaction
|
||||
///
|
||||
/// The debt will be split between people who are not in [whoPayed]
|
||||
DebtEntry({
|
||||
required this.id,
|
||||
required this.amount,
|
||||
required this.name,
|
||||
required this.whoPayed,
|
||||
}) : assert(whoPayed.isNotEmpty, "There has to be at least one payer");
|
||||
|
||||
/// Generates a class instance from a Map
|
||||
factory DebtEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$DebtEntryFromJson(json);
|
||||
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$DebtEntryToJson(this);
|
||||
|
||||
/// Unique identifier
|
||||
@JsonKey(required: true, disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
/// The payed amount
|
||||
@JsonKey(defaultValue: 0)
|
||||
int amount;
|
||||
|
||||
/// User-friendly identifier for the transaction
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// List of people who payed
|
||||
@JsonKey(defaultValue: _defaultDebtPayers)
|
||||
final List<DebtPerson> whoPayed;
|
||||
}
|
||||
|
||||
List<DebtPerson> _defaultDebtPayers() => [DebtPerson.unknownPerson()];
|
31
lib/api/debt_entry.g.dart
Normal file
31
lib/api/debt_entry.g.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'debt_entry.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DebtEntry _$DebtEntryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return DebtEntry(
|
||||
id: (json['id'] as num).toInt(),
|
||||
amount: (json['amount'] as num?)?.toInt() ?? 0,
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
whoPayed: (json['whoPayed'] as List<dynamic>?)
|
||||
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
_defaultDebtPayers(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$DebtEntryToJson(DebtEntry instance) => <String, dynamic>{
|
||||
'id': instance.id,
|
||||
'amount': instance.amount,
|
||||
'name': instance.name,
|
||||
'whoPayed': instance.whoPayed,
|
||||
};
|
33
lib/api/debt_person.dart
Normal file
33
lib/api/debt_person.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'debt_person.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
/// Represents a single person in a debt scenario
|
||||
class DebtPerson {
|
||||
/// Represents a single person in a debt scenario
|
||||
DebtPerson({required this.id, required this.name, this.bankAccount});
|
||||
|
||||
/// Default [DebtPerson] instance for json_serializable
|
||||
factory DebtPerson.unknownPerson() => DebtPerson(id: -1, name: "Unknown");
|
||||
|
||||
/// Generates a class instance from a Map
|
||||
factory DebtPerson.fromJson(Map<String, dynamic> json) =>
|
||||
_$DebtPersonFromJson(json);
|
||||
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$DebtPersonToJson(this);
|
||||
|
||||
/// Unique identifier
|
||||
@JsonKey(required: true, disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
/// Identifier that the user will see
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// Person's bank account
|
||||
///
|
||||
/// Used to generate a QR code payment
|
||||
String? bankAccount;
|
||||
}
|
27
lib/api/debt_person.g.dart
Normal file
27
lib/api/debt_person.g.dart
Normal file
|
@ -0,0 +1,27 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'debt_person.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DebtPerson _$DebtPersonFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return DebtPerson(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
bankAccount: json['bankAccount'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$DebtPersonToJson(DebtPerson instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'bankAccount': instance.bankAccount,
|
||||
};
|
64
lib/api/debt_scenario.dart
Normal file
64
lib/api/debt_scenario.dart
Normal file
|
@ -0,0 +1,64 @@
|
|||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/debt_entry.dart';
|
||||
import 'package:prasule/api/debt_person.dart';
|
||||
part 'debt_scenario.g.dart';
|
||||
|
||||
/// A folder for different entries and people
|
||||
@JsonSerializable()
|
||||
class DebtScenario {
|
||||
/// A folder for different entries and people
|
||||
DebtScenario({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.isArchived,
|
||||
required this.people,
|
||||
this.entries = const [],
|
||||
});
|
||||
|
||||
/// Generates a class instance from a Map
|
||||
factory DebtScenario.fromJson(Map<String, dynamic> json) =>
|
||||
_$DebtScenarioFromJson(json);
|
||||
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$DebtScenarioToJson(this);
|
||||
|
||||
/// Unique identifier
|
||||
@JsonKey(disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
/// User-friendly identifier
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// Whether this scenario should be shown under archived ones
|
||||
@JsonKey(defaultValue: false)
|
||||
bool isArchived;
|
||||
|
||||
/// All entries
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<DebtEntry> entries;
|
||||
|
||||
/// All people
|
||||
@JsonKey(defaultValue: _defaultPeopleList)
|
||||
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()];
|
36
lib/api/debt_scenario.g.dart
Normal file
36
lib/api/debt_scenario.g.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'debt_scenario.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
DebtScenario _$DebtScenarioFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return DebtScenario(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
isArchived: json['isArchived'] as bool? ?? false,
|
||||
entries: (json['entries'] as List<dynamic>?)
|
||||
?.map((e) => DebtEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
people: (json['people'] as List<dynamic>?)
|
||||
?.map((e) => DebtPerson.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
_defaultPeopleList(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$DebtScenarioToJson(DebtScenario instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'name': instance.name,
|
||||
'isArchived': instance.isArchived,
|
||||
'entries': instance.entries,
|
||||
'people': instance.people,
|
||||
};
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
part 'entry_data.g.dart';
|
||||
|
||||
|
@ -7,19 +11,25 @@ class EntryData {
|
|||
/// Contains raw data
|
||||
EntryData({required this.name, required this.amount, this.description = ""});
|
||||
|
||||
/// Connects the generated fromJson method
|
||||
/// Generates a class instance from a Map
|
||||
factory EntryData.fromJson(Map<String, dynamic> json) =>
|
||||
_$EntryDataFromJson(json);
|
||||
|
||||
/// [EntryData] instance used as a default value for json_serializable
|
||||
factory EntryData.unknown() => EntryData(name: "Unknown", amount: 0);
|
||||
|
||||
/// Name of entry
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
String name;
|
||||
|
||||
/// Optional description, default is empty
|
||||
@JsonKey(defaultValue: "")
|
||||
String description;
|
||||
|
||||
/// Amount for entry
|
||||
@JsonKey(defaultValue: 0)
|
||||
double amount;
|
||||
|
||||
/// Connects the generated toJson method
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$EntryDataToJson(this);
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ part of 'entry_data.dart';
|
|||
// **************************************************************************
|
||||
|
||||
EntryData _$EntryDataFromJson(Map<String, dynamic> json) => EntryData(
|
||||
name: json['name'] as String,
|
||||
amount: (json['amount'] as num).toDouble(),
|
||||
description: json['description'] as String? ?? "",
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
amount: (json['amount'] as num?)?.toDouble() ?? 0,
|
||||
description: json['description'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$EntryDataToJson(EntryData instance) => <String, dynamic>{
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
|
@ -20,21 +24,24 @@ class RecurringWalletEntry extends WalletSingleEntry {
|
|||
this.repeatAfter = 1,
|
||||
});
|
||||
|
||||
/// Connects the generated fromJson method
|
||||
/// Generates a class instance from a Map
|
||||
factory RecurringWalletEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$RecurringWalletEntryFromJson(json);
|
||||
|
||||
/// Connects the generated toJson method
|
||||
/// Converts the data in this instance into a Map
|
||||
@override
|
||||
Map<String, dynamic> toJson() => _$RecurringWalletEntryToJson(this);
|
||||
|
||||
/// Last date the recurring entry was added into the single entry list
|
||||
@JsonKey(defaultValue: DateTime.now)
|
||||
DateTime lastRunDate;
|
||||
|
||||
/// After how many {recurType} should the entry recur
|
||||
@JsonKey(defaultValue: 1)
|
||||
int repeatAfter;
|
||||
|
||||
/// What type of recurrence should happen
|
||||
@JsonKey(defaultValue: RecurType.month)
|
||||
RecurType recurType;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,19 +6,33 @@ part of 'recurring_entry.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
RecurringWalletEntry _$RecurringWalletEntryFromJson(
|
||||
Map<String, dynamic> json) =>
|
||||
RecurringWalletEntry(
|
||||
data: EntryData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
category:
|
||||
WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
id: json['id'] as int,
|
||||
lastRunDate: DateTime.parse(json['lastRunDate'] as String),
|
||||
repeatAfter: json['repeatAfter'] as int,
|
||||
recurType: $enumDecode(_$RecurTypeEnumMap, json['recurType']),
|
||||
);
|
||||
RecurringWalletEntry _$RecurringWalletEntryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return RecurringWalletEntry(
|
||||
data: json['data'] == null
|
||||
? EntryData.unknown()
|
||||
: EntryData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
type: $enumDecodeNullable(_$EntryTypeEnumMap, json['type']) ??
|
||||
EntryType.expense,
|
||||
date: json['date'] == null
|
||||
? DateTime.now()
|
||||
: DateTime.parse(json['date'] as String),
|
||||
category: json['category'] == null
|
||||
? WalletCategory.unknown()
|
||||
: WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
id: (json['id'] as num).toInt(),
|
||||
lastRunDate: json['lastRunDate'] == null
|
||||
? DateTime.now()
|
||||
: DateTime.parse(json['lastRunDate'] as String),
|
||||
recurType: $enumDecodeNullable(_$RecurTypeEnumMap, json['recurType']) ??
|
||||
RecurType.month,
|
||||
repeatAfter: (json['repeatAfter'] as num?)?.toInt() ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$RecurringWalletEntryToJson(
|
||||
RecurringWalletEntry instance) =>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:currency_picker/currency_picker.dart';
|
||||
|
@ -5,6 +9,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:json_annotation/json_annotation.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/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet_entry.dart';
|
||||
|
@ -27,34 +32,47 @@ class Wallet {
|
|||
this.categories = const [],
|
||||
this.entries = const [],
|
||||
this.recurringEntries = const [],
|
||||
this.debts = const [],
|
||||
this.starterBalance = 0,
|
||||
});
|
||||
|
||||
/// Connects the generated fromJson method
|
||||
/// Generates a class instance from a Map
|
||||
factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);
|
||||
|
||||
/// A list of all [RecurringWalletEntry]s
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<RecurringWalletEntry> recurringEntries;
|
||||
|
||||
/// Name of the wallet
|
||||
@JsonKey(defaultValue: "Unknown")
|
||||
final String name;
|
||||
|
||||
/// A list of available categories
|
||||
@JsonKey(defaultValue: _defaultWalletCategory)
|
||||
final List<WalletCategory> categories;
|
||||
|
||||
/// List of saved entries
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<WalletSingleEntry> entries;
|
||||
|
||||
/// List of user's [DebtScenario]s
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<DebtScenario> debts;
|
||||
|
||||
/// The starting balance of the wallet
|
||||
///
|
||||
/// Used to calculate current balance
|
||||
@JsonKey(defaultValue: 0)
|
||||
double starterBalance;
|
||||
|
||||
/// Selected currency
|
||||
@JsonKey(fromJson: _currencyFromJson)
|
||||
@JsonKey(
|
||||
fromJson: _currencyFromJson,
|
||||
defaultValue: _defaultCurrency,
|
||||
)
|
||||
final Currency currency;
|
||||
|
||||
/// Connects the generated toJson method
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$WalletToJson(this);
|
||||
|
||||
/// Getter for the next unused unique number ID in the wallet's **entry** list
|
||||
|
@ -76,6 +94,16 @@ class Wallet {
|
|||
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
|
||||
void recurEntries() {
|
||||
final n = DateTime.now();
|
||||
|
@ -147,7 +175,7 @@ class Wallet {
|
|||
///
|
||||
/// All [WalletSingleEntry]s will have their category reassigned
|
||||
/// to the default *No category*
|
||||
Future<void> removeCategory(WalletCategory category) async {
|
||||
void removeCategory(WalletCategory category) {
|
||||
// First remove the category from existing entries
|
||||
for (final entryToChange
|
||||
in entries.where((element) => element.category.id == category.id)) {
|
||||
|
@ -157,7 +185,7 @@ class Wallet {
|
|||
// Remove the category
|
||||
categories.removeWhere((element) => element.id == category.id);
|
||||
// Save
|
||||
await WalletManager.saveWallet(this);
|
||||
WalletManager.saveWallet(this);
|
||||
}
|
||||
|
||||
/// Returns the current balance
|
||||
|
@ -216,7 +244,7 @@ class Wallet {
|
|||
);
|
||||
|
||||
/// Creates test data used for debugging purposes
|
||||
Future<void> createTestEntries() async {
|
||||
void createTestEntries() {
|
||||
entries.clear();
|
||||
recurringEntries.clear();
|
||||
final random = Random();
|
||||
|
@ -274,6 +302,23 @@ class Wallet {
|
|||
);
|
||||
|
||||
// save and reload
|
||||
await WalletManager.saveWallet(this);
|
||||
WalletManager.saveWallet(this);
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletCategory> _defaultWalletCategory() => [WalletCategory.unknown()];
|
||||
Currency _defaultCurrency() => Currency.from(
|
||||
json: {
|
||||
"code": "USD",
|
||||
"name": "United States Dollar",
|
||||
"symbol": r"$",
|
||||
"flag": "USD",
|
||||
"decimal_digits": 2,
|
||||
"number": 840,
|
||||
"name_plural": "US dollars",
|
||||
"thousands_separator": ",",
|
||||
"decimal_separator": ".",
|
||||
"space_between_amount_and_symbol": false,
|
||||
"symbol_on_left": true,
|
||||
},
|
||||
);
|
||||
|
|
|
@ -7,22 +7,28 @@ part of 'wallet.dart';
|
|||
// **************************************************************************
|
||||
|
||||
Wallet _$WalletFromJson(Map<String, dynamic> json) => Wallet(
|
||||
name: json['name'] as String,
|
||||
currency: _currencyFromJson(json['currency'] as Map<String, dynamic>),
|
||||
name: json['name'] as String? ?? 'Unknown',
|
||||
currency: json['currency'] == null
|
||||
? _defaultCurrency()
|
||||
: _currencyFromJson(json['currency'] as Map<String, dynamic>),
|
||||
categories: (json['categories'] as List<dynamic>?)
|
||||
?.map((e) => WalletCategory.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
_defaultWalletCategory(),
|
||||
entries: (json['entries'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => WalletSingleEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
[],
|
||||
recurringEntries: (json['recurringEntries'] as List<dynamic>?)
|
||||
?.map((e) =>
|
||||
RecurringWalletEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
[],
|
||||
debts: (json['debts'] as List<dynamic>?)
|
||||
?.map((e) => DebtScenario.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
starterBalance: (json['starterBalance'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
|
||||
|
@ -31,6 +37,7 @@ Map<String, dynamic> _$WalletToJson(Wallet instance) => <String, dynamic>{
|
|||
'name': instance.name,
|
||||
'categories': instance.categories,
|
||||
'entries': instance.entries,
|
||||
'debts': instance.debts,
|
||||
'starterBalance': instance.starterBalance,
|
||||
'currency': instance.currency,
|
||||
};
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:prasule/api/category.dart';
|
||||
import 'package:prasule/api/entry_data.dart';
|
||||
|
@ -17,25 +21,30 @@ class WalletSingleEntry {
|
|||
required this.id,
|
||||
});
|
||||
|
||||
/// Connects the generated fromJson method
|
||||
/// Generates a class instance from a Map
|
||||
factory WalletSingleEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$WalletSingleEntryFromJson(json);
|
||||
|
||||
/// Expense or income
|
||||
@JsonKey(defaultValue: EntryType.expense)
|
||||
EntryType type;
|
||||
|
||||
/// Actual entry data
|
||||
@JsonKey(defaultValue: EntryData.unknown)
|
||||
EntryData data;
|
||||
|
||||
/// Date of entry creation
|
||||
@JsonKey(defaultValue: DateTime.now)
|
||||
DateTime date;
|
||||
|
||||
/// Selected category
|
||||
@JsonKey(defaultValue: WalletCategory.unknown)
|
||||
WalletCategory category;
|
||||
|
||||
/// Unique entry ID
|
||||
int id;
|
||||
@JsonKey(required: true, disallowNullValue: true)
|
||||
final int id;
|
||||
|
||||
/// Connects the generated toJson method
|
||||
/// Converts the data in this instance into a Map
|
||||
Map<String, dynamic> toJson() => _$WalletSingleEntryToJson(this);
|
||||
}
|
||||
|
|
|
@ -6,15 +6,27 @@ part of 'wallet_entry.dart';
|
|||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
WalletSingleEntry _$WalletSingleEntryFromJson(Map<String, dynamic> json) =>
|
||||
WalletSingleEntry(
|
||||
data: EntryData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
type: $enumDecode(_$EntryTypeEnumMap, json['type']),
|
||||
date: DateTime.parse(json['date'] as String),
|
||||
category:
|
||||
WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
id: json['id'] as int,
|
||||
);
|
||||
WalletSingleEntry _$WalletSingleEntryFromJson(Map<String, dynamic> json) {
|
||||
$checkKeys(
|
||||
json,
|
||||
requiredKeys: const ['id'],
|
||||
disallowNullValues: const ['id'],
|
||||
);
|
||||
return WalletSingleEntry(
|
||||
data: json['data'] == null
|
||||
? EntryData.unknown()
|
||||
: EntryData.fromJson(json['data'] as Map<String, dynamic>),
|
||||
type: $enumDecodeNullable(_$EntryTypeEnumMap, json['type']) ??
|
||||
EntryType.expense,
|
||||
date: json['date'] == null
|
||||
? DateTime.now()
|
||||
: DateTime.parse(json['date'] as String),
|
||||
category: json['category'] == null
|
||||
? WalletCategory.unknown()
|
||||
: WalletCategory.fromJson(json['category'] as Map<String, dynamic>),
|
||||
id: (json['id'] as num).toInt(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _$WalletSingleEntryToJson(WalletSingleEntry instance) =>
|
||||
<String, dynamic>{
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -11,18 +15,28 @@ import 'package:prasule/main.dart';
|
|||
|
||||
/// Used for [Wallet]-managing operations
|
||||
class WalletManager {
|
||||
/// Currently selected wallet
|
||||
static late Wallet selectedWallet;
|
||||
|
||||
/// Path to the directory with wallet files
|
||||
///
|
||||
/// Saved beforehand so we don't have to use async everywhere
|
||||
static late String walletPath;
|
||||
|
||||
/// Returns a list of all [Wallet]s
|
||||
static Future<List<Wallet>> listWallets() async {
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
static List<Wallet> listWallets() {
|
||||
final path = Directory(walletPath);
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
final wallets = <Wallet>[];
|
||||
for (final w
|
||||
in path.listSync().map((e) => e.path.split("/").last).toList()) {
|
||||
for (final w in path
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.map((e) => e.path.split("/").last)
|
||||
.toList()) {
|
||||
try {
|
||||
wallets.add(await loadWallet(w));
|
||||
wallets.add(loadWallet(w));
|
||||
} catch (e) {
|
||||
logger.e(e);
|
||||
// TODO: do something with unreadable wallets
|
||||
|
@ -32,9 +46,8 @@ class WalletManager {
|
|||
}
|
||||
|
||||
/// Deletes all [Wallet]s
|
||||
static Future<void> deleteAllData() async {
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
static void deleteAllData() {
|
||||
final path = Directory(walletPath);
|
||||
if (!path.existsSync()) {
|
||||
return;
|
||||
}
|
||||
|
@ -124,10 +137,10 @@ class WalletManager {
|
|||
d = File(filePath).readAsStringSync();
|
||||
}
|
||||
final w = Wallet.fromJson(jsonDecode(d) as Map<String, dynamic>);
|
||||
if (await WalletManager.exists(w.name)) {
|
||||
if (WalletManager.exists(w.name)) {
|
||||
throw Exception("Wallet already exists!");
|
||||
}
|
||||
await WalletManager.saveWallet(
|
||||
WalletManager.saveWallet(
|
||||
w,
|
||||
);
|
||||
}
|
||||
|
@ -166,15 +179,14 @@ class WalletManager {
|
|||
}
|
||||
|
||||
/// Loads and returns a single [Wallet] by name
|
||||
static Future<Wallet> loadWallet(String name) async {
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
static Wallet loadWallet(String name) {
|
||||
final path = Directory(walletPath);
|
||||
final wallet = File("${path.path}/$name");
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
if (!wallet.existsSync()) {
|
||||
return Future.error("Wallet does not exist");
|
||||
throw Exception("Wallet does not exist");
|
||||
}
|
||||
return Wallet.fromJson(
|
||||
jsonDecode(wallet.readAsStringSync()) as Map<String, dynamic>,
|
||||
|
@ -182,29 +194,26 @@ class WalletManager {
|
|||
}
|
||||
|
||||
/// Converts [Wallet] to JSON and saves it to AppData
|
||||
static Future<bool> saveWallet(Wallet w) async {
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
static void saveWallet(Wallet w) {
|
||||
final path = Directory(walletPath);
|
||||
final wallet = File("${path.path}/${w.name}");
|
||||
if (!path.existsSync()) {
|
||||
path.createSync();
|
||||
}
|
||||
// if (!wallet.existsSync()) return false;
|
||||
wallet.writeAsStringSync(jsonEncode(w.toJson()));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Deletes the corresponding [Wallet] file
|
||||
static Future<void> deleteWallet(Wallet w) async {
|
||||
final path =
|
||||
Directory("${(await getApplicationDocumentsDirectory()).path}/wallets");
|
||||
static void deleteWallet(Wallet w) {
|
||||
final path = Directory(walletPath);
|
||||
File("${path.path}/${w.name}").deleteSync();
|
||||
}
|
||||
|
||||
/// Checks if the wallet exists
|
||||
static Future<bool> exists(String name) async {
|
||||
static bool exists(String name) {
|
||||
return File(
|
||||
"${(await getApplicationDocumentsDirectory()).path}/wallets/$name",
|
||||
"$walletPath/$name",
|
||||
).existsSync();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,5 +122,14 @@
|
|||
"incomePerYearCategory":"Příjmy podle kategorie za rok {year}",
|
||||
"incomePerMonthCategory":"Příjmy podle kategorie za měsíc {monthYear}",
|
||||
"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"
|
||||
}
|
3
lib/l10n/app_cs.arb.license
Normal file
3
lib/l10n/app_cs.arb.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
|
@ -313,5 +313,14 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
}
|
3
lib/l10n/app_en.arb.license
Normal file
3
lib/l10n/app_en.arb.license
Normal file
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
|
||||
SPDX-License-Identifier: CC0-1.0
|
1
lib/l10n/app_sk.arb
Normal file
1
lib/l10n/app_sk.arb
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
@ -7,7 +11,7 @@ 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:prasule/views/initialization_screen.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
var _materialYou = false;
|
||||
|
@ -20,6 +24,7 @@ void main() async {
|
|||
}
|
||||
|
||||
_materialYou = s.getBool("useMaterialYou") ?? true;
|
||||
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
|
@ -39,6 +44,7 @@ class MyApp extends StatelessWidget {
|
|||
/// Override locale, used for testing
|
||||
final Locale? locale;
|
||||
// This widget is the root of your application.
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (Platform.isAndroid)
|
||||
|
@ -66,7 +72,7 @@ class MyApp extends StatelessWidget {
|
|||
colorScheme:
|
||||
_materialYou ? dark ?? darkColorScheme : darkColorScheme,
|
||||
),
|
||||
home: const HomeView(),
|
||||
home: const InitializationScreen(),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
@ -85,7 +91,7 @@ class MyApp extends StatelessWidget {
|
|||
...GlobalCupertinoLocalizations.delegates,
|
||||
],
|
||||
title: 'Prašule',
|
||||
home: HomeView(),
|
||||
home: InitializationScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
@ -6,26 +10,26 @@ 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,
|
||||
this.focusNode,
|
||||
this.inputBorder = const OutlineInputBorder(),
|
||||
this.suffix,
|
||||
this.prefix,
|
||||
});
|
||||
class PlatformField extends PlatformWidget<TextFormField, 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,
|
||||
this.focusNode,
|
||||
this.inputBorder = const OutlineInputBorder(),
|
||||
this.suffix,
|
||||
this.prefix,
|
||||
this.validator});
|
||||
final TextEditingController? controller;
|
||||
final bool? enabled;
|
||||
final bool obscureText;
|
||||
|
@ -42,9 +46,10 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
final FocusNode? focusNode;
|
||||
final Widget? suffix;
|
||||
final Widget? prefix;
|
||||
final String? Function(String?)? validator;
|
||||
|
||||
@override
|
||||
TextField createAndroidWidget(BuildContext context) => TextField(
|
||||
TextFormField createAndroidWidget(BuildContext context) => TextFormField(
|
||||
textAlign: textAlign,
|
||||
controller: controller,
|
||||
enabled: enabled,
|
||||
|
@ -63,6 +68,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
|||
onChanged: onChanged,
|
||||
autofillHints: autofillHints,
|
||||
maxLines: maxLines,
|
||||
validator: validator,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -20,11 +24,9 @@ const lightColorScheme = ColorScheme(
|
|||
errorContainer: Color(0xFFFFDAD6),
|
||||
onError: Color(0xFFFFFFFF),
|
||||
onErrorContainer: Color(0xFF410002),
|
||||
background: Color(0xFFFBFDF8),
|
||||
onBackground: Color(0xFF191C19),
|
||||
surface: Color(0xFFFBFDF8),
|
||||
onSurface: Color(0xFF191C19),
|
||||
surfaceVariant: Color(0xFFDCE5DB),
|
||||
surfaceContainerHighest: Color(0xFFDCE5DB),
|
||||
onSurfaceVariant: Color(0xFF414942),
|
||||
outline: Color(0xFF717971),
|
||||
onInverseSurface: Color(0xFFF0F1EC),
|
||||
|
@ -54,11 +56,9 @@ const darkColorScheme = ColorScheme(
|
|||
errorContainer: Color(0xFF93000A),
|
||||
onError: Color(0xFF690005),
|
||||
onErrorContainer: Color(0xFFFFDAD6),
|
||||
background: Color(0xFF191C19),
|
||||
onBackground: Color(0xFFE1E3DE),
|
||||
surface: Color(0xFF191C19),
|
||||
onSurface: Color(0xFFE1E3DE),
|
||||
surfaceVariant: Color(0xFF414942),
|
||||
surfaceContainerHighest: Color(0xFF414942),
|
||||
onSurfaceVariant: Color(0xFFC0C9BF),
|
||||
outline: Color(0xFF8B938A),
|
||||
onInverseSurface: Color(0xFF191C19),
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.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/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
|
||||
Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
||||
Drawer makeDrawer(BuildContext context, Pages page) => Drawer(
|
||||
child: ListView(
|
||||
children: [
|
||||
const DrawerHeader(child: Text("Prašule")),
|
||||
|
@ -15,9 +20,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).home,
|
||||
),
|
||||
selected: page == 1,
|
||||
selected: page == Pages.home,
|
||||
onTap: () {
|
||||
if (page == 1) {
|
||||
if (page == Pages.home) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -30,9 +35,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).graphs,
|
||||
),
|
||||
selected: page == 2,
|
||||
selected: page == Pages.graphs,
|
||||
onTap: () {
|
||||
if (page == 2) {
|
||||
if (page == Pages.graphs) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -45,9 +50,9 @@ Drawer makeDrawer(BuildContext context, int page) => Drawer(
|
|||
title: Text(
|
||||
AppLocalizations.of(context).recurringPayments,
|
||||
),
|
||||
selected: page == 3,
|
||||
selected: page == Pages.recurringEntries,
|
||||
onTap: () {
|
||||
if (page == 3) {
|
||||
if (page == Pages.recurringEntries) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
@ -56,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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/// Extension to get last day of the month
|
||||
extension LastDay on DateTime {
|
||||
/// Returns the last day of the month as [int]
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:currency_picker/currency_picker.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
|
@ -71,7 +75,8 @@ class ExpensesLineChart extends StatelessWidget {
|
|||
LineChartData(
|
||||
lineTouchData: LineTouchData(
|
||||
touchTooltipData: LineTouchTooltipData(
|
||||
tooltipBgColor: Theme.of(context).colorScheme.secondaryContainer,
|
||||
getTooltipColor: (group) =>
|
||||
Theme.of(context).colorScheme.secondaryContainer,
|
||||
getTooltipItems: (spots) => List<LineTooltipItem>.generate(
|
||||
spots.length,
|
||||
(index) => LineTooltipItem(
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:grouped_list/grouped_list.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Used to add [calculateTextColor] to the [Color] class
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -12,7 +16,7 @@ void showAbout(BuildContext context) {
|
|||
context: context,
|
||||
applicationLegalese: AppLocalizations.of(context).license,
|
||||
applicationName: "Prašule",
|
||||
applicationVersion: "1.1.0",
|
||||
applicationVersion: "1.1.1",
|
||||
applicationIcon: const CircleAvatar(
|
||||
backgroundImage: AssetImage("assets/icon/full_ico.png"),
|
||||
),
|
||||
|
|
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,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
@ -16,7 +20,8 @@ class CreateSingleEntryView extends StatefulWidget {
|
|||
/// Used when user wants to add new entry
|
||||
const CreateSingleEntryView({
|
||||
required this.w,
|
||||
required this.locale, super.key,
|
||||
required this.locale,
|
||||
super.key,
|
||||
this.editEntry,
|
||||
});
|
||||
|
||||
|
@ -28,6 +33,7 @@ class CreateSingleEntryView extends StatefulWidget {
|
|||
/// Is null unless we are editing an existing entry
|
||||
final WalletSingleEntry? editEntry;
|
||||
|
||||
/// Locale as set on user's system
|
||||
final String locale;
|
||||
|
||||
@override
|
||||
|
@ -36,6 +42,9 @@ class CreateSingleEntryView extends StatefulWidget {
|
|||
|
||||
class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
||||
late WalletSingleEntry newEntry;
|
||||
final _entryNameController = TextEditingController();
|
||||
final _entryBalanceController = TextEditingController();
|
||||
final _entryDescriptionController = TextEditingController();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -50,6 +59,9 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
category: widget.w.categories.first,
|
||||
);
|
||||
}
|
||||
_entryNameController.text = newEntry.data.name;
|
||||
_entryBalanceController.text = newEntry.data.amount.toString();
|
||||
_entryDescriptionController.text = newEntry.data.description;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
@ -71,10 +83,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).name,
|
||||
controller: TextEditingController(text: newEntry.data.name),
|
||||
onChanged: (v) {
|
||||
newEntry.data.name = v;
|
||||
},
|
||||
controller: _entryNameController,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -84,9 +93,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).amount,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.amount.toString(),
|
||||
),
|
||||
controller: _entryBalanceController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
|
@ -94,9 +101,6 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
newEntry.data.amount = double.parse(v);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -183,12 +187,7 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
child: PlatformField(
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.description,
|
||||
),
|
||||
onChanged: (v) {
|
||||
newEntry.data.description = v;
|
||||
},
|
||||
controller: _entryDescriptionController,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -197,10 +196,10 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
Text(AppLocalizations.of(context).date),
|
||||
PlatformButton(
|
||||
style: ButtonStyle(
|
||||
backgroundColor: MaterialStateProperty.all(
|
||||
backgroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
foregroundColor: MaterialStateProperty.all(
|
||||
foregroundColor: WidgetStateProperty.all(
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
|
@ -224,21 +223,25 @@ class _CreateSingleEntryViewState extends State<CreateSingleEntryView> {
|
|||
PlatformButton(
|
||||
text: AppLocalizations.of(context).save,
|
||||
onPressed: () {
|
||||
if (newEntry.data.name.isEmpty) {
|
||||
if (_entryNameController.text.isEmpty) {
|
||||
showMessage(
|
||||
AppLocalizations.of(context).errorEmptyName,
|
||||
context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
newEntry.data.name = _entryNameController.text;
|
||||
newEntry.data.amount =
|
||||
double.parse(_entryBalanceController.text);
|
||||
newEntry.data.description =
|
||||
_entryDescriptionController.text;
|
||||
if (widget.editEntry != null) {
|
||||
Navigator.of(context).pop(newEntry);
|
||||
return;
|
||||
}
|
||||
widget.w.entries.add(newEntry);
|
||||
WalletManager.saveWallet(widget.w).then(
|
||||
(value) => Navigator.of(context).pop(widget.w),
|
||||
); // TODO loading circle?
|
||||
WalletManager.saveWallet(widget.w); // TODO loading circle?
|
||||
Navigator.of(context).pop(widget.w);
|
||||
},
|
||||
),
|
||||
],
|
|
@ -1,716 +0,0 @@
|
|||
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>>[];
|
||||
|
||||
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;
|
||||
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("monthlygraph") ?? 2;
|
||||
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(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(),
|
||||
),
|
||||
)
|
||||
.then((value) async {
|
||||
selectedWallet =
|
||||
await WalletManager.loadWallet(selectedWallet!.name);
|
||||
final s = await SharedPreferences.getInstance();
|
||||
chartType = s.getInt("monthlygraph") ?? 2;
|
||||
setState(() {});
|
||||
});
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
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.background,
|
||||
),
|
||||
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.background,
|
||||
),
|
||||
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.background,
|
||||
),
|
||||
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.background,
|
||||
),
|
||||
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
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
|
@ -25,7 +29,7 @@ import 'package:prasule/util/drawer.dart';
|
|||
import 'package:prasule/util/sorting.dart';
|
||||
import 'package:prasule/util/text_color.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/tessdata_list.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
@ -63,8 +67,8 @@ class _HomeViewState extends State<HomeView> {
|
|||
loadWallet();
|
||||
}
|
||||
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
void loadWallet() {
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
|
@ -82,7 +86,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
return PopScope(
|
||||
canPop: !_searchActive, // don't pop when we just want
|
||||
// to deactivate searchfield
|
||||
onPopInvoked: (b) {
|
||||
onPopInvokedWithResult: (b, d) {
|
||||
if (b) return;
|
||||
_searchActive = false;
|
||||
_filter = "";
|
||||
|
@ -90,8 +94,10 @@ class _HomeViewState extends State<HomeView> {
|
|||
setState(() {});
|
||||
},
|
||||
child: Scaffold(
|
||||
drawer: makeDrawer(context, 1),
|
||||
drawer: makeDrawer(context, Pages.home),
|
||||
floatingActionButton: SpeedDial(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
|
@ -105,13 +111,12 @@ class _HomeViewState extends State<HomeView> {
|
|||
onTap: () {
|
||||
// debug option to quickly fill a wallet with data
|
||||
if (selectedWallet == null) return;
|
||||
selectedWallet!.createTestEntries().then((_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
platformRoute(
|
||||
(p0) => const HomeView(),
|
||||
),
|
||||
);
|
||||
});
|
||||
selectedWallet!.createTestEntries();
|
||||
Navigator.of(context).pushReplacement(
|
||||
platformRoute(
|
||||
(p0) => const HomeView(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SpeedDialChild(
|
||||
|
@ -166,7 +171,11 @@ class _HomeViewState extends State<HomeView> {
|
|||
secondChild: DropdownButton<int>(
|
||||
value: (selectedWallet == null)
|
||||
? -1
|
||||
: wallets.indexOf(selectedWallet!),
|
||||
: wallets.indexOf(
|
||||
wallets
|
||||
.where((w) => w.name == selectedWallet!.name)
|
||||
.first,
|
||||
),
|
||||
items: [
|
||||
...wallets.map(
|
||||
(e) => DropdownMenuItem(
|
||||
|
@ -190,7 +199,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
wallets = await WalletManager.listWallets();
|
||||
wallets = WalletManager.listWallets();
|
||||
selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
return;
|
||||
|
@ -244,9 +253,9 @@ class _HomeViewState extends State<HomeView> {
|
|||
(context) => const SettingsView(),
|
||||
),
|
||||
)
|
||||
.then((value) async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
selectedWallet = await WalletManager.loadWallet(
|
||||
.then((value) {
|
||||
wallets = WalletManager.listWallets();
|
||||
selectedWallet = WalletManager.loadWallet(
|
||||
selectedWallet!.name,
|
||||
);
|
||||
setState(() {});
|
||||
|
@ -355,7 +364,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
|
@ -405,7 +414,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onBackground,
|
||||
.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -590,7 +599,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.background
|
||||
.surface
|
||||
.calculateTextColor(),
|
||||
),
|
||||
),
|
||||
|
@ -626,11 +635,14 @@ class _HomeViewState extends State<HomeView> {
|
|||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push(
|
||||
platformRoute(
|
||||
(c) => const TessdataListView(),
|
||||
),
|
||||
)
|
||||
.then((value) => Navigator.of(c).pop());
|
||||
platformRoute(
|
||||
(c) => const TessdataListView(),
|
||||
),
|
||||
)
|
||||
.then((value) {
|
||||
if (!c.mounted) return;
|
||||
Navigator.of(c).pop();
|
||||
});
|
||||
},
|
||||
),
|
||||
PlatformButton(
|
||||
|
@ -737,7 +749,7 @@ class _HomeViewState extends State<HomeView> {
|
|||
);
|
||||
if (newEntry == null) return;
|
||||
selectedWallet!.entries.add(newEntry);
|
||||
await WalletManager.saveWallet(selectedWallet!);
|
||||
WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text("Ok"),
|
||||
|
|
48
lib/views/initialization_screen.dart
Normal file
48
lib/views/initialization_screen.dart
Normal file
|
@ -0,0 +1,48 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/pw/platformroute.dart';
|
||||
import 'package:prasule/views/home.dart';
|
||||
import 'package:prasule/views/setup.dart';
|
||||
|
||||
/// Intermediate screen used to initialize variables relying on context
|
||||
class InitializationScreen extends StatefulWidget {
|
||||
/// Intermediate screen used to initialize variables relying on context
|
||||
const InitializationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InitializationScreen> createState() => _InitializationScreenState();
|
||||
}
|
||||
|
||||
class _InitializationScreenState extends State<InitializationScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
getApplicationDocumentsDirectory().then((v) {
|
||||
WalletManager.walletPath = "${v.path}/wallets";
|
||||
if (!mounted) return;
|
||||
final wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const SetupView()));
|
||||
return;
|
||||
}
|
||||
WalletManager.selectedWallet = wallets.first;
|
||||
Navigator.of(context)
|
||||
.pushReplacement(platformRoute((c) => const HomeView()));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Align(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
@ -7,7 +11,6 @@ import 'package:prasule/api/entry_data.dart';
|
|||
import 'package:prasule/api/recurring_entry.dart';
|
||||
import 'package:prasule/api/wallet.dart';
|
||||
import 'package:prasule/api/wallet_manager.dart';
|
||||
import 'package:prasule/main.dart';
|
||||
import 'package:prasule/pw/platformbutton.dart';
|
||||
import 'package:prasule/pw/platformfield.dart';
|
||||
import 'package:prasule/util/show_message.dart';
|
||||
|
@ -39,6 +42,10 @@ class CreateRecurringEntryView extends StatefulWidget {
|
|||
|
||||
class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
||||
late RecurringWalletEntry newEntry;
|
||||
final _entryNameController = TextEditingController();
|
||||
final _entryBalanceController = TextEditingController();
|
||||
final _entryDescriptionController = TextEditingController();
|
||||
final _repeatAfterController = TextEditingController();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -55,6 +62,10 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
recurType: RecurType.month,
|
||||
);
|
||||
}
|
||||
_entryNameController.text = newEntry.data.name;
|
||||
_entryBalanceController.text = newEntry.data.amount.toString();
|
||||
_entryDescriptionController.text = newEntry.data.description;
|
||||
_repeatAfterController.text = newEntry.repeatAfter.toString();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
|
@ -76,10 +87,7 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).name,
|
||||
controller: TextEditingController(text: newEntry.data.name),
|
||||
onChanged: (v) {
|
||||
newEntry.data.name = v;
|
||||
},
|
||||
controller: _entryNameController,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -89,9 +97,7 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: PlatformField(
|
||||
labelText: AppLocalizations.of(context).amount,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.amount.toString(),
|
||||
),
|
||||
controller: _entryBalanceController,
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
inputFormatters: [
|
||||
|
@ -99,10 +105,6 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
RegExp(r'\d+[\.,]{0,1}\d{0,}'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
logger.i(v);
|
||||
newEntry.data.amount = double.parse(v);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -189,12 +191,7 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
child: PlatformField(
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
controller: TextEditingController(
|
||||
text: newEntry.data.description,
|
||||
),
|
||||
onChanged: (v) {
|
||||
newEntry.data.description = v;
|
||||
},
|
||||
controller: _entryDescriptionController,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
|
@ -215,9 +212,7 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
SizedBox(
|
||||
width: 50,
|
||||
child: PlatformField(
|
||||
controller: TextEditingController(
|
||||
text: newEntry.repeatAfter.toString(),
|
||||
),
|
||||
controller: _repeatAfterController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
FilteringTextInputFormatter.deny(
|
||||
|
@ -313,21 +308,27 @@ class _CreateRecurringEntryViewState extends State<CreateRecurringEntryView> {
|
|||
PlatformButton(
|
||||
text: AppLocalizations.of(context).save,
|
||||
onPressed: () {
|
||||
if (newEntry.data.name.isEmpty) {
|
||||
if (_entryNameController.text.isEmpty) {
|
||||
showMessage(
|
||||
AppLocalizations.of(context).errorEmptyName,
|
||||
context,
|
||||
);
|
||||
return;
|
||||
}
|
||||
newEntry.data.name = _entryNameController.text;
|
||||
newEntry.data.amount =
|
||||
double.parse(_entryBalanceController.text);
|
||||
newEntry.repeatAfter =
|
||||
int.parse(_repeatAfterController.text);
|
||||
newEntry.data.description =
|
||||
_entryDescriptionController.text;
|
||||
if (widget.editEntry != null) {
|
||||
Navigator.of(context).pop(newEntry);
|
||||
return;
|
||||
}
|
||||
widget.w.recurringEntries.add(newEntry);
|
||||
WalletManager.saveWallet(widget.w).then(
|
||||
(value) => Navigator.of(context).pop(widget.w),
|
||||
); // TODO loading circle?
|
||||
WalletManager.saveWallet(widget.w); // TODO loading circle?
|
||||
Navigator.of(context).pop(widget.w);
|
||||
},
|
||||
),
|
||||
],
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -13,7 +17,7 @@ import 'package:prasule/pw/platformroute.dart';
|
|||
import 'package:prasule/util/drawer.dart';
|
||||
import 'package:prasule/util/text_color.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/setup.dart';
|
||||
|
||||
|
@ -44,8 +48,8 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
|||
loadWallet();
|
||||
}
|
||||
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
void loadWallet() {
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
|
@ -60,11 +64,14 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
drawer: makeDrawer(context, 3),
|
||||
drawer: makeDrawer(context, Pages.recurringEntries),
|
||||
appBar: AppBar(
|
||||
title: DropdownButton<int>(
|
||||
value:
|
||||
(selectedWallet == null) ? -1 : wallets.indexOf(selectedWallet!),
|
||||
value: (selectedWallet == null)
|
||||
? -1
|
||||
: wallets.indexOf(
|
||||
wallets.where((w) => w.name == selectedWallet!.name).first,
|
||||
),
|
||||
items: [
|
||||
...wallets.map(
|
||||
(e) => DropdownMenuItem(
|
||||
|
@ -88,7 +95,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
wallets = await WalletManager.listWallets();
|
||||
wallets = WalletManager.listWallets();
|
||||
selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
return;
|
||||
|
@ -113,7 +120,7 @@ class _RecurringEntriesViewState extends State<RecurringEntriesView> {
|
|||
)
|
||||
.then((value) async {
|
||||
selectedWallet =
|
||||
await WalletManager.loadWallet(selectedWallet!.name);
|
||||
WalletManager.loadWallet(selectedWallet!.name);
|
||||
});
|
||||
} else if (value == AppLocalizations.of(context).about) {
|
||||
showAbout(context);
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -39,7 +43,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
}
|
||||
|
||||
Future<void> loadWallet() async {
|
||||
wallets = await WalletManager.listWallets();
|
||||
wallets = WalletManager.listWallets();
|
||||
if (wallets.isEmpty && mounted) {
|
||||
unawaited(
|
||||
Navigator.of(context)
|
||||
|
@ -82,7 +86,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
wallets = await WalletManager.listWallets();
|
||||
wallets = WalletManager.listWallets();
|
||||
logger.i(wallets.length);
|
||||
selectedWallet = wallets.last;
|
||||
setState(() {});
|
||||
|
@ -122,7 +126,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
textAlign: TextAlign.center,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
selectedWallet!.categories.add(
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context)
|
||||
|
@ -137,7 +141,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
await WalletManager.saveWallet(selectedWallet!);
|
||||
WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
|
@ -206,7 +210,7 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
),
|
||||
),
|
||||
);
|
||||
await WalletManager.saveWallet(selectedWallet!);
|
||||
WalletManager.saveWallet(selectedWallet!);
|
||||
setState(() {});
|
||||
},
|
||||
child: Container(
|
||||
|
@ -226,8 +230,8 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.cancel),
|
||||
onPressed: () async {
|
||||
await selectedWallet!.removeCategory(
|
||||
onPressed: () {
|
||||
selectedWallet!.removeCategory(
|
||||
selectedWallet!.categories[i],
|
||||
);
|
||||
setState(() {});
|
||||
|
@ -243,11 +247,11 @@ class _EditCategoriesViewState extends State<EditCategoriesView> {
|
|||
builder: (c) => AlertDialog.adaptive(
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
onPressed: () {
|
||||
if (controller.text.isEmpty) return;
|
||||
selectedWallet!.categories[i].name =
|
||||
controller.text;
|
||||
await WalletManager.saveWallet(
|
||||
WalletManager.saveWallet(
|
||||
selectedWallet!,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
@ -37,7 +41,7 @@ class _GraphTypeSettingsViewState extends State<GraphTypeSettingsView> {
|
|||
body: SettingsList(
|
||||
applicationType: ApplicationType.both,
|
||||
darkTheme: SettingsThemeData(
|
||||
settingsListBackground: Theme.of(context).colorScheme.background,
|
||||
settingsListBackground: Theme.of(context).colorScheme.surface,
|
||||
titleTextColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
sections: [
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -42,7 +46,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
body: SettingsList(
|
||||
applicationType: ApplicationType.both,
|
||||
darkTheme: SettingsThemeData(
|
||||
settingsListBackground: Theme.of(context).colorScheme.background,
|
||||
settingsListBackground: Theme.of(context).colorScheme.surface,
|
||||
titleTextColor: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
sections: [
|
||||
|
@ -117,7 +121,7 @@ class _SettingsViewState extends State<SettingsView> {
|
|||
description:
|
||||
Text(AppLocalizations.of(context).exportSingleDesc),
|
||||
onPressed: (ctx) async {
|
||||
final all = await WalletManager.listWallets();
|
||||
final all = WalletManager.listWallets();
|
||||
if (!ctx.mounted) return;
|
||||
final w = await showAdaptiveDialog<String>(
|
||||
context: ctx,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -49,60 +53,57 @@ class _SetupViewState extends State<SetupView> {
|
|||
},
|
||||
);
|
||||
List<WalletCategory> categories = <WalletCategory>[];
|
||||
String name = "";
|
||||
final _nameController = TextEditingController();
|
||||
final _balanceController = TextEditingController(text: "0.0");
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (categories.isEmpty) {
|
||||
name = AppLocalizations.of(context).setupNamePlaceholder;
|
||||
categories = [
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).noCategory,
|
||||
id: 0,
|
||||
icon: IconData(
|
||||
Icons.payments.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
if (categories.isNotEmpty) return;
|
||||
_nameController.text = AppLocalizations.of(context).setupNamePlaceholder;
|
||||
categories = [
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).noCategory,
|
||||
id: 0,
|
||||
icon: IconData(
|
||||
Icons.payments.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryHealth,
|
||||
id: 1,
|
||||
icon: IconData(
|
||||
Icons.medical_information.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
color: Colors.red.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryHealth,
|
||||
id: 1,
|
||||
icon: IconData(
|
||||
Icons.medical_information.codePoint,
|
||||
fontFamily: 'MaterialIcons',
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryCar,
|
||||
id: 2,
|
||||
icon:
|
||||
IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color: Colors.purple
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryFood,
|
||||
id: 3,
|
||||
icon:
|
||||
IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color: Colors.green.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryTravel,
|
||||
id: 4,
|
||||
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color: Colors.orange.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
];
|
||||
setState(() {});
|
||||
}
|
||||
color: Colors.red.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryCar,
|
||||
id: 2,
|
||||
icon: IconData(Icons.car_repair.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color:
|
||||
Colors.purple.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryFood,
|
||||
id: 3,
|
||||
icon: IconData(Icons.restaurant.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color: Colors.green.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
WalletCategory(
|
||||
name: AppLocalizations.of(context).categoryTravel,
|
||||
id: 4,
|
||||
icon: IconData(Icons.train.codePoint, fontFamily: 'MaterialIcons'),
|
||||
color: Colors.orange.shade700
|
||||
.harmonizeWith(Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
];
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -134,8 +135,8 @@ class _SetupViewState extends State<SetupView> {
|
|||
next: Text(AppLocalizations.of(context).next),
|
||||
back: Text(AppLocalizations.of(context).back),
|
||||
done: Text(AppLocalizations.of(context).finish),
|
||||
onDone: () async {
|
||||
if (name.isEmpty) {
|
||||
onDone: () {
|
||||
if (_nameController.text.isEmpty) {
|
||||
unawaited(
|
||||
showMessage(
|
||||
AppLocalizations.of(context).errorEmptyName,
|
||||
|
@ -144,7 +145,8 @@ class _SetupViewState extends State<SetupView> {
|
|||
);
|
||||
return;
|
||||
}
|
||||
if (await WalletManager.exists(name) && context.mounted) {
|
||||
if (WalletManager.exists(_nameController.text) &&
|
||||
context.mounted) {
|
||||
unawaited(
|
||||
showMessage(
|
||||
AppLocalizations.of(context).walletExists,
|
||||
|
@ -154,17 +156,20 @@ class _SetupViewState extends State<SetupView> {
|
|||
return;
|
||||
}
|
||||
final wallet = Wallet(
|
||||
name: name,
|
||||
name: _nameController.text,
|
||||
currency: _selectedCurrency,
|
||||
categories: categories,
|
||||
starterBalance: double.parse(_balanceController.text),
|
||||
);
|
||||
await WalletManager.saveWallet(wallet);
|
||||
WalletManager.saveWallet(wallet);
|
||||
|
||||
if (widget.newWallet && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
WalletManager.selectedWallet = wallet;
|
||||
|
||||
if (!context.mounted) return;
|
||||
unawaited(
|
||||
Navigator.of(context).pushReplacement(
|
||||
|
@ -232,15 +237,7 @@ class _SetupViewState extends State<SetupView> {
|
|||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: PlatformField(
|
||||
controller: TextEditingController(
|
||||
text:
|
||||
AppLocalizations.of(context).setupNamePlaceholder,
|
||||
),
|
||||
onChanged: (t) {
|
||||
name = t;
|
||||
},
|
||||
),
|
||||
child: PlatformField(controller: _nameController),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
|
|
269
pubspec.lock
269
pubspec.lock
|
@ -5,34 +5,39 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
|
||||
sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "67.0.0"
|
||||
version: "68.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.1.5"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
|
||||
sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
version: "6.5.0"
|
||||
archive:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: archive
|
||||
sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.10"
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
|
||||
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -69,10 +74,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: build_daemon
|
||||
sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
|
||||
sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
version: "4.0.2"
|
||||
build_resolvers:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -85,18 +90,18 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.11"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
|
||||
sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.3.0"
|
||||
version: "7.3.1"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -109,10 +114,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
|
||||
sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.1"
|
||||
version: "8.9.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -173,10 +178,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76"
|
||||
sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.7.2"
|
||||
version: "1.8.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -189,34 +194,34 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.8"
|
||||
currency_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: currency_picker
|
||||
sha256: eb75deb7bc92e3f31e1b8ad4efacf71371e8e49d7a0eebd1c1a8e9fae58cc23d
|
||||
sha256: c9ab5c0da8ae4dcc6421064dccde072b44f5b44914691b967ea4cf45a266a658
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.20"
|
||||
version: "2.0.21"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
|
||||
sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.4"
|
||||
version: "2.3.6"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
|
||||
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.1"
|
||||
version: "5.4.3+1"
|
||||
dots_indicator:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -229,10 +234,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: dynamic_color
|
||||
sha256: a866f1f8947bfdaf674d7928e769eac7230388a2e7a2542824fad4bb5b87be3b
|
||||
sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.9"
|
||||
version: "1.7.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -277,26 +282,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "00b74ae680df6b1135bdbea00a7d1fc072a9180b7c3f3702e4b19a9943f5ed7d"
|
||||
sha256: "2b7c1f5d867da9a054661641c8f499c55c47c39acccb97b3bc673f5fa9a39e74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.66.2"
|
||||
version: "0.67.0"
|
||||
flex_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flex_color_picker
|
||||
sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf"
|
||||
sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.5.1"
|
||||
flex_seed_scheme:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flex_seed_scheme
|
||||
sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
|
||||
sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "3.0.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -327,10 +332,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_keyboard_visibility
|
||||
sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb"
|
||||
sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.1"
|
||||
version: "6.0.0"
|
||||
flutter_keyboard_visibility_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -379,14 +384,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -396,10 +393,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_slidable
|
||||
sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c"
|
||||
sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "3.1.0"
|
||||
flutter_speed_dial:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -412,10 +409,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_tesseract_ocr
|
||||
sha256: "4a8d0e3f562ee01d94a464ff9d31d9e907b1e374aeff29bf696f979417c70bcf"
|
||||
sha256: c3af11e3a8803b36bd24f7ec8ff3e0e21168dac917407357ae177ddf9fe10180
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.24"
|
||||
version: "0.4.26"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -430,10 +427,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: fluttertoast
|
||||
sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1
|
||||
sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.2.4"
|
||||
version: "8.2.6"
|
||||
font_awesome_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -446,10 +443,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "4.0.0"
|
||||
fuchsia_remote_debug_protocol:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -499,10 +496,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.7"
|
||||
version: "4.2.0"
|
||||
integration_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -512,18 +509,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.18.1"
|
||||
version: "0.19.0"
|
||||
introduction_screen:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: introduction_screen
|
||||
sha256: "72d25ceb71471773783f72783608e17585af93d4bc6474df577fcfe9e7842852"
|
||||
sha256: "325f26e86fa3c3e86e6ab2bbc1fda860c9e6eae5ff29166fc2a3cab8f710d5b5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.12"
|
||||
version: "3.1.14"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -544,58 +541,50 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
|
||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.1"
|
||||
version: "6.8.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: cdd14e3836065a1f6302a236ec8b5f700695c803c57ae11a1c84df31e6bcf831
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.3"
|
||||
version: "10.0.5"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9b2ef90589911d665277464e0482b209d39882dffaaf4ef69a3561a3354b2ebc"
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.5"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: fd3cd66cb2bcd7b50dcd3b413af49d78051f809c8b3f6e047962765c15a0d23d
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
version: "3.0.1"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
|
||||
sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2+1"
|
||||
version: "2.3.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -604,6 +593,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
macros:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: a8403c89b36483b4cbf9f1fcd24562f483cb34a5c9bf101cf2b0d8a083cf1239
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.0-main.5"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -616,18 +613,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
version: "0.11.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.14.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -672,26 +669,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b
|
||||
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.3"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
|
||||
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.6"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
|
||||
sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -740,14 +737,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pointycastle
|
||||
sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.7.4"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -768,10 +757,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.1"
|
||||
version: "6.1.2"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -784,10 +773,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
||||
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.3.0"
|
||||
settings_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -800,26 +789,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
version: "2.2.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.3"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.5"
|
||||
version: "2.4.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -880,10 +869,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
|
||||
sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
version: "2.0.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -981,26 +970,26 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: test
|
||||
sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073"
|
||||
sha256: d11b55850c68c1f6c0cf00eabded4e66c4043feaf6c0d7ce4a36785137df6331
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.25.2"
|
||||
version: "1.25.5"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
version: "0.7.1"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4"
|
||||
sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.6.2"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1021,26 +1010,26 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e"
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
version: "6.3.0"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
version: "6.3.3"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.5"
|
||||
version: "6.3.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1053,10 +1042,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.2.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1069,10 +1058,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d"
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1093,18 +1082,18 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: very_good_analysis
|
||||
sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8"
|
||||
sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "6.0.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246
|
||||
sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.0.0"
|
||||
version: "14.2.2"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1117,18 +1106,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.5.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2"
|
||||
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "3.0.0"
|
||||
webdriver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1157,10 +1154,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.4"
|
||||
version: "5.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1186,5 +1183,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: prasule
|
||||
description: Open-source private expense tracker
|
||||
|
||||
version: 1.1.0+7
|
||||
version: 1.1.1+8
|
||||
|
||||
environment:
|
||||
sdk: '>=3.1.0-262.2.beta <4.0.0'
|
||||
|
@ -18,7 +18,7 @@ dependencies:
|
|||
currency_picker: ^2.0.16
|
||||
dio: ^5.3.0
|
||||
dynamic_color: ^1.6.6
|
||||
fl_chart: ^0.66.0
|
||||
fl_chart: ^0.67.0
|
||||
flex_color_picker: ^3.3.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
@ -28,7 +28,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
flutter_slidable: ^3.0.0
|
||||
flutter_speed_dial: ^7.0.0
|
||||
flutter_tesseract_ocr: ^0.4.23
|
||||
flutter_tesseract_ocr: ^0.4.26
|
||||
fluttertoast: ^8.2.4
|
||||
grouped_list: ^5.1.2
|
||||
intl: any
|
||||
|
@ -51,13 +51,12 @@ dev_dependencies:
|
|||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^3.0.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
test: ^1.24.6
|
||||
very_good_analysis: ^5.1.0
|
||||
very_good_analysis: ^6.0.0
|
||||
yaml: ^3.1.2
|
||||
|
||||
flutter_launcher_icons:
|
||||
|
|
6
sonar-project.properties
Normal file
6
sonar-project.properties
Normal file
|
@ -0,0 +1,6 @@
|
|||
sonar.projectKey=prasule
|
||||
sonar.sources=lib,pubspec.yaml
|
||||
sonar.tests=integration_test,test_driver
|
||||
sonar.dart.analyzer.mode=FLUTTER
|
||||
sonar.dart.analyzer.options.override=true
|
||||
sonar.host.url=https://sq.mnau.xyz
|
3
test_driver/integration_test.dart
Normal file
3
test_driver/integration_test.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
import 'package:integration_test/integration_test_driver.dart';
|
||||
|
||||
Future<void> main() => integrationDriver();
|
Loading…
Reference in a new issue