Compare commits

...

64 commits
main ... dev

Author SHA1 Message Date
513a276031 chore(deps): update dependency very_good_analysis to v6 (#46)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [very_good_analysis](https://github.com/VeryGoodOpenSource/very_good_analysis) | dev_dependencies | major | `^5.1.0` -> `^6.0.0` |

>  **Important**
>
> Release Notes retrieval for this PR were skipped because no github.com credentials were available.
> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻️ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy40MzIuMCIsInVwZGF0ZWRJblZlciI6IjM3LjQzMi4wIiwidGFyZ2V0QnJhbmNoIjoiZGV2IiwibGFiZWxzIjpbXX0=-->

Reviewed-on: #46
Co-authored-by: Renovator <kontakt+renovator@caras.wtf>
Co-committed-by: Renovator <kontakt+renovator@caras.wtf>
2024-08-18 22:14:59 +02:00
Matyáš Caras
2d50509163
Added translation using Weblate (Slovak) 2024-07-19 17:41:44 +02:00
ef52caa836
feat: creating debt scenarios
also changed way of loading selected wallet
2024-07-19 17:34:45 +02:00
300f359070
feat!: change app id 2024-07-18 18:31:19 +02:00
6530b58c19
chore: restructure views 2024-07-18 14:44:07 +02:00
2339739ff8
docs: add badges and remove playstore link 2024-07-18 14:35:03 +02:00
5f99e35709
ci: change analyzer mode 2024-07-18 13:21:52 +02:00
fd04bc1dc4
ci: set correct test folder 2024-07-18 13:15:10 +02:00
1b7eac52ee
chore: bump deps 2024-07-18 12:25:24 +02:00
2cc628ce89
ci: remove tweaks from podfile 2024-07-18 12:16:01 +02:00
68a89444dd
fix: revert ios build tweaks 2024-07-18 12:15:50 +02:00
4cd4913d35
Revert "fix: ios inconsistency"
This reverts commit b909dd27dd.
2024-07-18 12:13:29 +02:00
8665db6c0f
ci: remove sq CI 2024-07-18 12:05:43 +02:00
6a5ddedffd
ci: safe dir
Some checks failed
/ build (push) Failing after 10m6s
2024-07-18 00:26:14 +02:00
dbcc7ad972
ci: set flutter channel manually
Some checks failed
/ build (push) Failing after 4m52s
2024-07-18 00:18:01 +02:00
d7dbf4fff0
ci: flutter path?
Some checks failed
/ build (push) Failing after 7m28s
2024-07-18 00:10:07 +02:00
01b9decfdc
ci: no adb wait
Some checks failed
/ build (push) Failing after 6m55s
2024-07-17 23:59:46 +02:00
5733f52572
ci: another emulator path
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:57:17 +02:00
66848fdc2d
ci: try putting tools in path manually
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:47:37 +02:00
031d9c2ea3
ci: download emulator too
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:29:49 +02:00
537b520288
ci: actually save bin to path
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:21:26 +02:00
4808a142aa
ci: mkdir
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:14:41 +02:00
1c2ef630c3
ci: no gunzip
Some checks failed
/ build (push) Failing after 2m23s
2024-07-17 22:11:27 +02:00
ecc14affe8
ci: actually install nodejs
Some checks failed
/ build (push) Failing after 3m5s
2024-07-17 22:06:18 +02:00
cb5dbeffcf
ci: set up stuff manually
Some checks failed
/ build (push) Has been cancelled
2024-07-17 22:01:58 +02:00
1d1391d0df
ci: add node set up due to failing ci
Some checks failed
/ build (push) Failing after 15s
2024-07-17 21:42:20 +02:00
32432d760c
ci: try putting test and AVD together
Some checks failed
/ build (push) Failing after 2m58s
2024-07-17 21:36:49 +02:00
ba3b963d84
ci: don't blindly copypaste code
Some checks failed
/ build (push) Has been cancelled
2024-07-17 20:20:25 +02:00
e1d3a54ebf
ci: basic sonarqube action
Some checks failed
/ build (push) Failing after 3m7s
2024-07-17 20:11:44 +02:00
8e46b936a8
fix: set default values through json key 2024-07-17 17:37:41 +02:00
d93eac4f04
fix: create basic debt class structure 2024-07-17 16:44:02 +02:00
94eb563217
chore: bump target version 2024-07-16 20:34:41 +02:00
cddb58c118
docs: add license header 2024-06-30 20:25:36 +02:00
62e0c2ba3d
fix: only check json files in listWallets 2024-06-30 11:30:33 +02:00
07908797d5
feat: use less Futures 2024-06-30 11:30:07 +02:00
1855ed4e6b
ci: ignore generated files 2024-06-30 11:08:34 +02:00
09d5bbb953
chore: upgrade flutter 2024-06-30 10:45:11 +02:00
Matyáš Caras
80f2ae9f7f
docs: add app bar fix to changelog 2024-04-22 17:04:59 +02:00
Matyáš Caras
481716a3b2
chore: bump version 2024-04-22 17:03:51 +02:00
Matyáš Caras
8ff63c9492
fix: set correct graph type when opening graph view
fixes #41
2024-04-22 16:59:08 +02:00
Matyáš Caras
1faa5e2f78
fix: don't display 'Add new wallet' when returning from a screen 2024-04-22 16:36:20 +02:00
Matyáš Caras
5cbb9a12dc
fix: remove deprecated code 2024-04-22 16:25:59 +02:00
Matyáš Caras
58dd06f39f
fix: use controllers for textfields
Fixes #39
2024-04-22 16:18:48 +02:00
Matyáš Caras
d832080a6d
fix: implement breaking change from fl_chart 2024-04-22 15:53:38 +02:00
Matyáš Caras
a90a39feff fix(deps): update dependency fl_chart to ^0.67.0 (#42)
Reviewed-on: #42
2024-04-22 15:52:32 +02:00
41d45e96fb fix(deps): update dependency fl_chart to ^0.67.0 2024-04-22 15:37:21 +02:00
Matyáš Caras
3ff35fd427
fix: wrong text in wallet setup name field
#40
2024-04-22 15:06:18 +02:00
Matyáš Caras
f7b6219d31
Merge branch 'main' into dev 2024-04-22 14:47:57 +02:00
Matyáš Caras
638b6eef67
docs: add info to CONTRIBUTING 2024-03-07 18:59:50 +01:00
Matyáš Caras
92e185dcb1 Merge branch 'main' into dev 2024-02-26 23:35:30 +01:00
Matyáš Caras
85fefda4d8
chore: bump version in about dialog 2024-02-26 23:33:43 +01:00
Matyáš Caras
7c8535b99d
chore: bump version 2024-02-26 23:30:45 +01:00
Matyáš Caras
15d2590a8e
docs: update changelog 2024-02-26 23:30:31 +01:00
Matyáš Caras
bd17e12e1b
fix: show correct indicator on pie chart 2024-02-26 23:26:11 +01:00
Matyáš Caras
af04accabf
fix: update state after creating recurring entry 2024-02-26 23:26:02 +01:00
Matyáš Caras
c7e40b12b7
fix: icons in iconpicker per platform 2024-02-26 22:59:08 +01:00
Matyáš Caras
4a3f097b02
fix: showing created entry after OCR 2024-02-26 22:58:01 +01:00
Matyáš Caras
e5e803057a
fix: localization mistakes 2024-02-26 22:53:51 +01:00
Matyáš Caras
a6872408fa
fix: set kotlin version everywhere 2024-02-26 22:53:38 +01:00
Matyáš Caras
4d7144267e
fix: downgrade iconpicker 2024-02-26 22:53:10 +01:00
Matyáš Caras
aeda571f35
chore: upgrade flutter 2024-02-26 21:58:18 +01:00
Matyáš Caras
776e9b056a
Merge branch 'main' into dev 2024-02-26 21:50:24 +01:00
Matyáš Caras
7dbf4c969a
Merge branch 'dev' of https://git.mnau.xyz/hernik/prasule into dev 2024-02-12 19:28:17 +01:00
Matyáš Caras
76da494804
chore: require full screen on ios 2024-02-12 19:28:10 +01:00
68 changed files with 2236 additions and 1158 deletions

@ -1 +1 @@
Subproject commit 1751123cde4ffad08ae27bdee4f8ddebd033fe76
Subproject commit 2feea7a4071e25c1e3aac9c17016531bc4442f2a

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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`

View file

@ -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
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
SPDX-License-Identifier: AGPL-3.0-only

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
SPDX-License-Identifier: AGPL-3.0-only

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: (C) 2024 Tesseract-OCR contributors
SPDX-License-Identifier: Apache-2.0

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
SPDX-License-Identifier: AGPL-3.0-only

View file

@ -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(

View file

@ -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)";

View file

@ -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;

View file

@ -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
View 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
View 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
View 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;
}

View 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,
};

View 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()];

View 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,
};

View file

@ -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);
}

View file

@ -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>{

View file

@ -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;
}

View file

@ -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) =>

View file

@ -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,
},
);

View file

@ -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,
};

View file

@ -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);
}

View file

@ -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>{

View file

@ -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();
}
}

View file

@ -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"
}

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
SPDX-License-Identifier: CC0-1.0

View file

@ -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"
}

View 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
View file

@ -0,0 +1 @@
{}

View file

@ -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(),
),
);
}

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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';

View file

@ -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';

View file

@ -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),

View file

@ -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
}

View file

@ -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]

View file

@ -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(

View file

@ -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';

View file

@ -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';

View file

@ -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

View file

@ -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"),
),

View 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(),
),
),
),
);
}
}

View 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),
),
],
),
),
),
),
),
);
}
}

View file

@ -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);
},
),
],

View file

@ -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
],
),
),
);
}
}

View 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
],
),
),
);
}
}

View file

@ -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"),

View 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(),
),
);
}
}

View file

@ -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);
},
),
],

View file

@ -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);

View file

@ -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;

View file

@ -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: [

View file

@ -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,

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: (C) 2024 Matyáš Caras
//
// SPDX-License-Identifier: AGPL-3.0-only
import 'dart:async';
import 'dart:io';

View file

@ -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,

View file

@ -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"

View file

@ -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
View 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

View file

@ -0,0 +1,3 @@
import 'package:integration_test/integration_test_driver.dart';
Future<void> main() => integrationDriver();