185 Commits

Author SHA1 Message Date
66c97dabb1 Merge pull request 'Update Rust crate serde to 1.0.228' (#111) from renovate/serde-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-10-07 00:12:52 +00:00
84e0fec3a2 Update Rust crate serde to 1.0.228
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-10-06 00:14:10 +00:00
11fb50af9a Merge pull request 'Update Rust crate log to 0.4.28' (#110) from renovate/log-0.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-10-05 00:14:59 +00:00
76d1430ecb Update Rust crate log to 0.4.28
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-10-04 00:37:04 +00:00
22848bcec4 Merge pull request 'Update react to ^19.2.0' (#109) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-10-03 00:39:04 +00:00
d2f948c8fa Update react to ^19.2.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-10-02 00:38:17 +00:00
05aff4bbfb Merge pull request 'Update dependency @types/react to ^19.1.15' (#108) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-30 00:35:25 +00:00
74b1a4dbcc Update dependency @types/react to ^19.1.15
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-29 00:36:02 +00:00
8ad7b589ce Merge pull request 'Update Rust crate clap to 4.5.48' (#107) from renovate/clap-4.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-27 00:37:25 +00:00
ff20cb7bc9 Update Rust crate clap to 4.5.48
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-26 00:35:29 +00:00
1c345127b4 Merge pull request 'Update Rust crate anyhow to 1.0.100' (#106) from renovate/anyhow-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-24 00:37:17 +00:00
71e54b8d72 Update Rust crate anyhow to 1.0.100
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-22 00:39:15 +00:00
13a8bbad1d Merge pull request 'Update Rust crate actix-files to 0.6.8' (#105) from renovate/actix-files-0.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-21 00:40:54 +00:00
ec212902e9 Update Rust crate actix-files to 0.6.8
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-20 00:36:36 +00:00
6786ed6075 Merge pull request 'Update dependency @fontsource/roboto to ^5.2.8' (#104) from renovate/fontsource-roboto-5.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-19 00:35:32 +00:00
c881f58cf4 Update dependency @fontsource/roboto to ^5.2.8
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-18 00:36:43 +00:00
586204dcba Merge pull request 'Update dependency @fontsource/roboto to ^5.2.7' (#103) from renovate/fontsource-roboto-5.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-17 00:35:53 +00:00
da56f99ce6 Update dependency @fontsource/roboto to ^5.2.7
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-16 00:35:52 +00:00
57d2b1bf6a Merge pull request 'Update dependency @types/react to ^19.1.13' (#102) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-15 00:36:19 +00:00
831ccd073c Update dependency @types/react to ^19.1.13
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-14 00:36:35 +00:00
c5269a587b Merge pull request 'Update Rust crate chrono to 0.4.42' (#101) from renovate/chrono-0.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-13 00:37:13 +00:00
71ce57d205 Update Rust crate chrono to 0.4.42
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-12 00:36:04 +00:00
d57107cfc1 Merge pull request 'Update dependency vite to ^6.3.6' (#100) from renovate/vite-6.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-11 00:35:11 +00:00
68a3eb4218 Update dependency vite to ^6.3.6
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-10 00:35:14 +00:00
514f140527 Merge pull request 'Update Rust crate clap to 4.5.47' (#99) from renovate/clap-4.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-09 00:35:25 +00:00
4bac50b676 Update Rust crate clap to 4.5.47
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-08 00:36:10 +00:00
884b86daa9 Merge pull request 'Update Rust crate actix-files to 0.6.7' (#98) from renovate/actix-files-0.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-07 00:36:19 +00:00
64365916bd Update Rust crate actix-files to 0.6.7
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-06 00:35:57 +00:00
77c0640ec0 Merge pull request 'Update dependency ts-pattern to ^5.8.0' (#97) from renovate/ts-pattern-5.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-05 00:22:01 +00:00
96c80eb18c Update dependency ts-pattern to ^5.8.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-04 00:29:07 +00:00
0cde9f5635 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.9' (#96) from renovate/eslint-plugin-react-x-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-03 00:27:57 +00:00
5e35dae02f Update dependency eslint-plugin-react-x to ^1.52.9
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-09-02 00:28:11 +00:00
f321376990 Merge pull request 'Update dependency dayjs to ^1.11.18' (#95) from renovate/dayjs-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-09-01 00:28:05 +00:00
86b86d4d68 Update dependency dayjs to ^1.11.18
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-31 00:26:59 +00:00
35c629a339 Merge pull request 'Update react' (#94) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-30 00:27:37 +00:00
da0d5adcb9 Update react
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-29 00:28:23 +00:00
2214387010 Merge pull request 'Update dependency scanbot_sdk to ^7.0.1' (#93) from renovate/scanbot_sdk-7.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-26 00:27:16 +00:00
667ce69be8 Update dependency scanbot_sdk to ^7.0.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-08-26 00:27:14 +00:00
57f4ed53f6 Merge pull request 'Update dependency flutter_hooks to ^0.21.3+1' (#92) from renovate/flutter_hooks-0.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-25 00:27:49 +00:00
deb884a1f0 Update dependency flutter_hooks to ^0.21.3+1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-25 00:27:44 +00:00
0e5d878e30 Merge pull request 'Update dependency @types/react to ^19.1.11' (#91) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-24 00:27:01 +00:00
b266cbcadb Update dependency @types/react to ^19.1.11
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-23 00:28:07 +00:00
91bca2b6b1 Merge pull request 'Update dependency @mui/x-charts to ^8.10.2' (#90) from renovate/mui-x-charts-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-22 00:27:27 +00:00
a741662251 Update dependency @mui/x-charts to ^8.10.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-21 00:27:57 +00:00
34f0493c51 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.6' (#89) from renovate/eslint-plugin-react-x-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-20 00:27:47 +00:00
b538b6fcb3 Update dependency eslint-plugin-react-x to ^1.52.6
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-19 00:27:39 +00:00
9d18d975d0 Merge pull request 'Update dependency @mui/x-charts to ^8.10.1' (#87) from renovate/mui-x-charts-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-17 00:27:47 +00:00
43049bc229 Update dependency @mui/x-charts to ^8.10.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-16 00:26:40 +00:00
19ca17b43a Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.4' (#86) from renovate/eslint-plugin-react-x-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-15 00:28:03 +00:00
572046f418 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.4' (#85) from renovate/eslint-plugin-react-dom-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-15 00:28:02 +00:00
24af473dd3 Update dependency eslint-plugin-react-x to ^1.52.4
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-14 00:27:47 +00:00
8a57c57ec4 Update dependency eslint-plugin-react-dom to ^1.52.4
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-14 00:27:44 +00:00
50a5e7745f Merge pull request 'Update Rust crate anyhow to 1.0.99' (#84) from renovate/anyhow-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-13 00:29:06 +00:00
675e4d9ecd Merge pull request 'Update dependency @types/react to ^19.1.10' (#83) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-13 00:28:05 +00:00
83c214af7d Update Rust crate anyhow to 1.0.99
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-12 00:28:57 +00:00
bfa6af5749 Update dependency @types/react to ^19.1.10
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-12 00:27:22 +00:00
6ab157504c Merge pull request 'Update dependency @mui/x-charts to ^8.10.0' (#82) from renovate/mui-x-charts-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-11 00:28:40 +00:00
bb98ea5e46 Merge pull request 'Update dependency @eslint/js to ^9.33.0' (#81) from renovate/eslint-js-9.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-11 00:28:37 +00:00
2d104a54b5 Update dependency @mui/x-charts to ^8.10.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-10 00:28:25 +00:00
3b0ff29bc8 Update dependency @eslint/js to ^9.33.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-10 00:28:22 +00:00
4ade72a0ee Merge pull request 'Update dependency build_runner to ^2.6.1' (#80) from renovate/build_runner-2.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-09 00:27:37 +00:00
6021b44a13 Update dependency build_runner to ^2.6.1
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-09 00:27:30 +00:00
eb92e8c0c5 Merge pull request 'Update dependency eslint to ^9.32.0' (#79) from renovate/eslint-9.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-08 00:16:25 +00:00
d98305908c Merge pull request 'Update Rust crate clap to 4.5.43' (#78) from renovate/clap-4.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-08 00:16:22 +00:00
ae5ef99e3a Update dependency eslint to ^9.32.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-07 00:17:08 +00:00
2f592183e4 Update Rust crate clap to 4.5.43
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-07 00:17:06 +00:00
74291a258c Merge pull request 'Update Rust crate serde_json to 1.0.142' (#77) from renovate/serde_json-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-06 00:16:05 +00:00
df8cd6a046 Merge pull request 'Update dependency @mui/x-date-pickers to ^8.9.2' (#76) from renovate/mui-x-date-pickers-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-06 00:15:04 +00:00
079fbbf154 Update Rust crate serde_json to 1.0.142
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-05 00:16:16 +00:00
ba443629e6 Update dependency @mui/x-date-pickers to ^8.9.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-05 00:14:32 +00:00
2c07a69b90 Merge pull request 'Update dependency @mui/x-data-grid to ^8.9.2' (#75) from renovate/mui-x-data-grid-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-04 00:15:26 +00:00
0de551f1de Update dependency @mui/x-data-grid to ^8.9.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-04 00:15:21 +00:00
2488ef0125 Merge pull request 'Update dependency @mui/x-charts to ^8.9.2' (#74) from renovate/mui-x-charts-8.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-03 00:17:29 +00:00
aa2e764262 Update dependency @mui/x-charts to ^8.9.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-08-02 00:17:07 +00:00
1202219e98 Merge pull request 'Update Rust crate clap to 4.5.42' (#73) from renovate/clap-4.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-01 00:20:44 +00:00
112597084c Merge pull request 'Update react' (#72) from renovate/react into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-01 00:20:43 +00:00
4f64404ffa Update Rust crate clap to 4.5.42
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-31 00:15:45 +00:00
c39b53c721 Update react
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-31 00:15:41 +00:00
bd2e343601 Show API URL when a token has been created
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-30 21:39:32 +02:00
85ee2b2549 Ignore auto login variable if it is empty
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-30 19:47:34 +02:00
154551aeaf Merge pull request 'Update dependency build_runner to ^2.6.0' (#71) from renovate/build_runner-2.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-30 00:15:47 +00:00
7b10c3508a Update dependency build_runner to ^2.6.0
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-30 00:15:41 +00:00
61c96629a1 Merge pull request 'Update dependency eslint-plugin-react-dom to ^1.52.3' (#37) from renovate/eslint-plugin-react-dom-1.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-30 00:14:59 +00:00
8644075a09 Update dependency eslint-plugin-react-dom to ^1.52.3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-29 00:17:21 +00:00
81bfa75eec Redirect to list screens after a successful scan
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-28 21:47:00 +02:00
de0dd4e36a Merge pull request 'Update dependency @vitejs/plugin-react to ^4.7.0' (#70) from renovate/vitejs-plugin-react-4.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-28 00:16:36 +00:00
f9d7a63738 Update dependency @vitejs/plugin-react to ^4.7.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-28 00:16:33 +00:00
0ef6f8288f Merge pull request 'Update dependency @mui/x-date-pickers to ^8.9.0' (#69) from renovate/mui-x-date-pickers-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-27 00:17:07 +00:00
2f23e4dadb Update dependency @mui/x-date-pickers to ^8.9.0
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-27 00:17:04 +00:00
5cf5fac8f4 Merge pull request 'Update dependency @eslint/js to ^9.32.0' (#68) from renovate/eslint-js-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-26 00:16:48 +00:00
8e143db354 Update dependency @eslint/js to ^9.32.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-26 00:16:46 +00:00
1237c9706e Merge pull request 'Update dependency @mui/x-data-grid to ^8.9.1' (#67) from renovate/mui-x-data-grid-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-25 00:17:11 +00:00
1add0b4cfe Update dependency @mui/x-data-grid to ^8.9.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-25 00:17:07 +00:00
6920d6d9b0 Merge pull request 'Update dependency @mui/x-charts to ^8.9.0' (#66) from renovate/mui-x-charts-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-24 00:17:01 +00:00
27e92660f1 Update dependency @mui/x-charts to ^8.9.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-24 00:16:58 +00:00
743e5ba410 Merge pull request 'Update Rust crate rand to 0.9.2' (#65) from renovate/rand-0.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-23 00:17:12 +00:00
8039b1c807 Update Rust crate rand to 0.9.2
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-23 00:17:07 +00:00
9ef84ba63a Merge pull request 'Update dependency eslint to ^9.31.0' (#8) from renovate/eslint-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-23 00:16:37 +00:00
56e5ae6629 Update dependency eslint to ^9.31.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-22 00:19:56 +00:00
4443131516 Can download APK from web app
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-21 19:48:32 +02:00
365d7589b1 Force refresh of expenses editor
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 19:55:18 +02:00
23cc189e53 Fix date extraction
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-20 19:17:47 +02:00
3098d12e8a Support short dates
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-20 18:32:02 +02:00
0943104cc8 Can show expense in full screen
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 18:23:37 +02:00
3beaba806a Can clear cost value quickly 2025-07-20 18:18:20 +02:00
1788e7f184 Can disable dates extraction 2025-07-20 18:14:03 +02:00
71d32d72ef Can extract date of expenses
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 18:07:22 +02:00
28f61a3099 Improve regular expression
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-20 17:44:46 +02:00
f61e3541fb Perform first OCR extraction 2025-07-20 17:25:52 +02:00
fb7891d913 Merge pull request 'Update Rust crate serde_json to 1.0.141' (#63) from renovate/serde_json-1.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-20 00:15:22 +00:00
d9ede224cf Update Rust crate serde_json to 1.0.141
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-20 00:15:16 +00:00
fc9334b20b Merge pull request 'Update dependency dart to ^3.8.2' (#62) from renovate/dart-3.x into main
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-19 00:18:13 +00:00
c4cbd7ec8b Update dependency dart to ^3.8.2
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-19 00:17:10 +00:00
a4ef3e74dc Optimize APK size
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-07-18 18:37:58 +02:00
dbb988f2b5 Add mobile application (#47)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #47
2025-07-18 16:06:21 +00:00
b2aff4902d Fix Flutter code quality issues
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-18 18:01:25 +02:00
6f578b39f9 Updated project dependencies
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-18 17:57:57 +02:00
de519ecb6c Fix generated APK name
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is passing
2025-07-18 11:25:19 +02:00
3049e545e9 Fix keystore script file
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-18 10:08:51 +02:00
1f1c01a287 Sign APK builds from CI
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-18 09:04:01 +02:00
92885b8af6 Merge pull request 'Update dependency globals to ^16.3.0' (#61) from renovate/globals-16.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-18 00:51:18 +00:00
44320db760 Update dependency globals to ^16.3.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-18 00:16:24 +00:00
1f2a28aa65 Fix signing configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 22:58:59 +02:00
f9566315eb Create a publish flavor dedicated for public releases
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 22:50:12 +02:00
63bed07015 Change debug configuration
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 20:36:44 +02:00
4b84d926d4 Disallow app backup
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-17 20:32:13 +02:00
8191a28986 Fix Drone configuration
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 20:27:24 +02:00
8c30b50d0c Update
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 20:25:31 +02:00
389b2c96ba Remove temporary comments in Drone configuration
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-17 19:51:01 +02:00
5a08b0c971 Test full APK build before release
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is passing
2025-07-17 19:31:39 +02:00
b3fd066633 Fix clone
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build is passing
2025-07-17 19:26:18 +02:00
5c987473a5 Unshallow Git clone
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2025-07-17 19:25:27 +02:00
c3d2612f9a Second test
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 19:21:49 +02:00
130cc1ef0d Temp test
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 19:20:44 +02:00
aebefd114a Reorganize mobile app building 2025-07-17 19:19:20 +02:00
34d3e08149 Improve pipeline
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-07-17 19:08:21 +02:00
ccd3540804 Remove mobile app dependencies
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 18:53:12 +02:00
b9b871224b Add flutter build to CI
Some checks reported errors
continuous-integration/drone/pr Build was killed
continuous-integration/drone/push Build was killed
2025-07-17 18:52:16 +02:00
17a22d7a4c Add an option to start on scans screen instead of capture screen
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 18:04:03 +02:00
8db2cf3ece Show a message when expenses list is empty
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-17 17:51:13 +02:00
e45648e038 Fix: reset editor screen between captures 2025-07-17 17:49:01 +02:00
55144da943 Can upload expenses to server 2025-07-17 17:44:36 +02:00
5065f780f2 Merge pull request 'Update dependency @mui/x-date-pickers to ^8.8.0' (#60) from renovate/mui-x-date-pickers-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-17 00:46:19 +00:00
28d8b96ebe Update dependency @mui/x-date-pickers to ^8.8.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-17 00:16:12 +00:00
baf62aa2a5 Fix misleading comment
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-16 11:30:25 +00:00
8a9a8d6b14 Merge remote-tracking branch 'origin/main' into mobile-app
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-16 11:06:52 +02:00
c19d46a50f Merge pull request 'Update dependency @mui/x-data-grid to ^8.8.0' (#59) from renovate/mui-x-data-grid-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-16 00:39:44 +00:00
f001c618cd Update dependency @mui/x-data-grid to ^8.8.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-16 00:15:46 +00:00
f9d46e46a5 Fix ESLint issue
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-15 21:34:19 +02:00
96f1bf589c Can clear search filter in account page
Some checks failed
continuous-integration/drone/push Build is failing
2025-07-15 21:33:19 +02:00
8ec6e48938 Start to build synchronization logic
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-15 21:14:09 +02:00
235fda5c72 Can delete expense 2025-07-15 20:11:43 +02:00
2568ea14b4 Can update scanned expenses entries 2025-07-15 20:00:55 +02:00
467393dad0 Merge pull request 'Update dependency @mui/x-charts to ^8.8.0' (#58) from renovate/mui-x-charts-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-15 00:36:12 +00:00
f619f26e93 Update dependency @mui/x-charts to ^8.8.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-15 00:15:36 +00:00
cecb7a0cd1 Remove temporary fix on label style
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-14 16:50:53 +02:00
50812af2fc Display the list of expenses 2025-07-14 16:49:32 +02:00
547e9b7aad Can save expenses to local list
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 16:24:59 +02:00
dd035f8a15 Fix PDF rendering on my smartphone
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 16:03:45 +02:00
768706e2d4 Add expense editor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 10:06:07 +02:00
70023242e9 Start to build expense editor screen 2025-07-14 09:42:42 +02:00
951338b6e4 Display generated PDF on expense screen 2025-07-14 09:15:18 +02:00
6531d73c93 Ready to implement expense editor screen
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 08:18:59 +02:00
51ba649b6e Can save the list of expenses 2025-07-14 08:00:35 +02:00
cc4ce19af2 Merge pull request 'Update dependency @eslint/js to ^9.31.0' (#57) from renovate/eslint-js-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-14 00:38:25 +00:00
192dc5827b Update dependency @eslint/js to ^9.31.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-14 00:15:23 +00:00
37674a6229 Merge pull request 'Update dependency eslint-plugin-react-x to ^1.52.3' (#56) from renovate/eslint-plugin-react-x-1.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-13 00:40:31 +00:00
ef86667029 Update dependency eslint-plugin-react-x to ^1.52.3
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-13 00:15:07 +00:00
07f63a96fa Merge pull request 'Update Rust crate diesel to 2.2.12' (#55) from renovate/diesel-2.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-12 00:47:30 +00:00
fa88a3c9ed Update Rust crate diesel to 2.2.12
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-12 00:15:34 +00:00
85c6a0b955 Merge pull request 'Update dependency @mui/x-date-pickers to ^8.7.0' (#54) from renovate/mui-x-date-pickers-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-11 00:41:02 +00:00
21ee97b8a4 Update dependency @mui/x-date-pickers to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-11 00:15:43 +00:00
119f026a21 Merge pull request 'Update Rust crate clap to 4.5.41' (#53) from renovate/clap-4.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-10 00:56:57 +00:00
d72acfac9b Update Rust crate clap to 4.5.41
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2025-07-10 00:16:25 +00:00
77c8866bb8 Merge pull request 'Update dependency @mui/x-data-grid to ^8.7.0' (#52) from renovate/mui-x-data-grid-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-09 00:44:36 +00:00
133f235639 Update dependency @mui/x-data-grid to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-09 00:15:58 +00:00
7ef0499abf Merge pull request 'Update dependency @mui/x-charts to ^8.7.0' (#51) from renovate/mui-x-charts-8.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-08 00:45:34 +00:00
1383da4483 Update dependency @mui/x-charts to ^8.7.0
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-08 00:16:00 +00:00
4f1a9d0865 Merge pull request 'Update dependency @eslint/js to ^9.30.1' (#50) from renovate/eslint-js-9.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-07 00:40:39 +00:00
31803feaa9 Update dependency @eslint/js to ^9.30.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-07-07 00:15:37 +00:00
aad32f9c25 Merge pull request 'Update dependency react-router-dom to ^7.6.3' (#49) from renovate/react-router-dom-7.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-06 00:42:40 +00:00
1ea2bd6acf Update dependency react-router-dom to ^7.6.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-06 00:15:52 +00:00
a085116018 Merge pull request 'Update dependency react-router to ^7.6.3' (#48) from renovate/react-router-7.x into main
All checks were successful
continuous-integration/drone/push Build is passing
2025-07-05 00:50:00 +00:00
952a66042c Update dependency react-router to ^7.6.3
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-05 00:09:30 +00:00
6cf6ab5a37 Update dependency @emotion/styled to ^11.14.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-07-04 00:09:24 +00:00
41 changed files with 2968 additions and 1564 deletions

View File

@@ -4,9 +4,17 @@ type: docker
name: default name: default
steps: steps:
# Needs a full git clone
- name: fetch
image: alpine/git
commands:
- git fetch --tags
# Frontend # Frontend
- name: web_build - name: web_build
image: node:23 image: node:23
depends_on:
- fetch
volumes: volumes:
- name: web_app - name: web_app
path: /tmp/web_build path: /tmp/web_build
@@ -20,6 +28,8 @@ steps:
# Backend # Backend
- name: backend_fetch_deps - name: backend_fetch_deps
image: rust image: rust
depends_on:
- fetch
volumes: volumes:
- name: rust_registry - name: rust_registry
path: /usr/local/cargo/registry path: /usr/local/cargo/registry
@@ -54,6 +64,9 @@ steps:
- name: backend_build - name: backend_build
image: rust image: rust
when:
event:
- tag
volumes: volumes:
- name: rust_registry - name: rust_registry
path: /usr/local/cargo/registry path: /usr/local/cargo/registry
@@ -72,12 +85,58 @@ steps:
- ls -lah target/release/moneymgr_backend target/release/examples/api_curl - ls -lah target/release/moneymgr_backend target/release/examples/api_curl
- cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release - cp target/release/moneymgr_backend target/release/examples/api_curl /tmp/release
# Mobile app code quality
- name: mobile_app_code_quality
image: ghcr.io/cirruslabs/flutter:latest
depends_on:
- fetch
commands:
- echo "Build version:" $(git describe --tags --abbrev=0)
- echo "Build number:" $(git rev-list --count $(git describe --tags --abbrev=0))
- cd moneymgr_mobile
- flutter --disable-analytics
- flutter pub get --enforce-lockfile
- dart run build_runner build
- flutter analyze
# Mobile app build
- name: mobile_app_build
image: ghcr.io/cirruslabs/flutter:latest
depends_on:
- backend_build # prevent synchronous backend & frontend build
- mobile_app_code_quality
when:
event:
- tag
environment:
JKS_KEYSTORE:
from_secret: JKS_KEYSTORE
JKS_KEYSTORE_PASSWORD:
from_secret: JKS_KEYSTORE_PASSWORD
volumes:
- name: release
path: /tmp/release
commands:
- cd moneymgr_mobile
- flutter --disable-analytics
- bash android/ci_write_keystore.sh
- flutter pub get --enforce-lockfile
- dart run build_runner build
- flutter build apk
--release
--flavor publish
--build-name $(git describe --tags --abbrev=0)
--split-per-abi
--target-platform android-arm64
--build-number $(git rev-list --count $(git describe --tags --abbrev=0))
- cp build/app/outputs/flutter-apk/app-arm64-v8a-publish-release.apk /tmp/release/moneymgr_mobile_arm64-v8a.apk
# Release # Release
- name: gitea_release - name: gitea_release
image: plugins/gitea-release image: plugins/gitea-release
depends_on: depends_on:
- backend_build - backend_build
- mobile_app_build
when: when:
event: event:
- tag - tag

View File

@@ -36,9 +36,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-files" name = "actix-files"
version = "0.6.6" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" checksum = "6c0d87f10d70e2948ad40e8edea79c8e77c6c66e0250a4c1f09b690465199576"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-service", "actix-service",
@@ -46,7 +46,7 @@ dependencies = [
"actix-web", "actix-web",
"bitflags", "bitflags",
"bytes", "bytes",
"derive_more 0.99.20", "derive_more 2.0.1",
"futures-core", "futures-core",
"http-range", "http-range",
"log", "log",
@@ -87,7 +87,7 @@ dependencies = [
"mime", "mime",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"rand 0.9.1", "rand 0.9.2",
"sha1", "sha1",
"smallvec", "smallvec",
"tokio", "tokio",
@@ -364,12 +364,6 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -431,9 +425,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.98" version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
@@ -700,16 +694,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link 0.2.0",
] ]
[[package]] [[package]]
@@ -724,9 +717,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.40" version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -734,9 +727,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.40" version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -746,9 +739,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.40" version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -1100,9 +1093,9 @@ dependencies = [
[[package]] [[package]]
name = "diesel" name = "diesel"
version = "2.2.11" version = "2.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614" checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"byteorder", "byteorder",
@@ -2157,9 +2150,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.27" version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]] [[package]]
name = "lzma-rs" name = "lzma-rs"
@@ -2298,7 +2291,7 @@ dependencies = [
"light-openid", "light-openid",
"log", "log",
"mime_guess", "mime_guess",
"rand 0.9.1", "rand 0.9.2",
"rust-embed", "rust-embed",
"rust-s3", "rust-s3",
"rust_xlsxwriter", "rust_xlsxwriter",
@@ -2741,9 +2734,9 @@ dependencies = [
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.1" version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [ dependencies = [
"rand_chacha 0.9.0", "rand_chacha 0.9.0",
"rand_core 0.9.3", "rand_core 0.9.3",
@@ -3212,18 +3205,28 @@ checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.219" version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3232,9 +3235,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.140" version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -3994,7 +3997,7 @@ checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link", "windows-link 0.1.1",
"windows-result", "windows-result",
"windows-strings 0.4.1", "windows-strings 0.4.1",
] ]
@@ -4027,6 +4030,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]] [[package]]
name = "windows-registry" name = "windows-registry"
version = "0.4.0" version = "0.4.0"
@@ -4044,7 +4053,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -4053,7 +4062,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]
@@ -4062,7 +4071,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.1.1",
] ]
[[package]] [[package]]

View File

@@ -5,26 +5,26 @@ edition = "2024"
[dependencies] [dependencies]
env_logger = "0.11.8" env_logger = "0.11.8"
log = "0.4.27" log = "0.4.28"
diesel = { version = "2.2.11", features = ["postgres", "r2d2"] } diesel = { version = "2.2.12", features = ["postgres", "r2d2"] }
diesel_migrations = "2.2.0" diesel_migrations = "2.2.0"
clap = { version = "4.5.40", features = ["env", "derive"] } clap = { version = "4.5.48", features = ["env", "derive"] }
actix-web = "4.11.0" actix-web = "4.11.0"
actix-cors = "0.7.1" actix-cors = "0.7.1"
actix-multipart = "0.7.2" actix-multipart = "0.7.2"
actix-remote-ip = "0.1.0" actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.1", features = ["redis-session"] } actix-session = { version = "0.10.1", features = ["redis-session"] }
actix-files = "0.6.6" actix-files = "0.6.8"
lazy_static = "1.5.0" lazy_static = "1.5.0"
anyhow = "1.0.98" anyhow = "1.0.100"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
rust-s3 = "0.36.0-beta.2" rust-s3 = "0.36.0-beta.2"
thiserror = "2.0.12" thiserror = "2.0.12"
tokio = "1.45.1" tokio = "1.45.1"
futures-util = "0.3.31" futures-util = "0.3.31"
serde_json = "1.0.140" serde_json = "1.0.142"
light-openid = "1.0.4" light-openid = "1.0.4"
rand = "0.9.1" rand = "0.9.2"
ipnet = { version = "2.11.0", features = ["serde"] } ipnet = { version = "2.11.0", features = ["serde"] }
lazy-regex = "3.4.1" lazy-regex = "3.4.1"
jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] }
@@ -33,7 +33,7 @@ rust-embed = { version = "8.7.2" }
sha2 = "0.11.0-rc.0" sha2 = "0.11.0-rc.0"
base16ct = "0.2.0" base16ct = "0.2.0"
httpdate = "1.0.3" httpdate = "1.0.3"
chrono = "0.4.41" chrono = "0.4.42"
tempfile = "3.20.0" tempfile = "3.20.0"
zip = "3.0.0" zip = "3.0.0"
rust_xlsxwriter = "0.87.0" rust_xlsxwriter = "0.87.0"

View File

@@ -29,7 +29,7 @@ pub struct AppConfig {
/// Unsecure : for development, bypass authentication, using the account with the given /// Unsecure : for development, bypass authentication, using the account with the given
/// email address by default /// email address by default
#[clap(long, env)] #[clap(long, env)]
pub unsecure_auto_login_email: Option<String>, unsecure_auto_login_email: Option<String>,
/// PostgreSQL database host /// PostgreSQL database host
#[clap(long, env, default_value = "localhost")] #[clap(long, env, default_value = "localhost")]
@@ -126,6 +126,14 @@ pub struct AppConfig {
/// Redis password /// Redis password
#[clap(long, env, default_value = "secretredis")] #[clap(long, env, default_value = "secretredis")]
redis_password: String, redis_password: String,
/// Application download URL
#[clap(
long,
env,
default_value = "https://gitea.communiquons.org/pierre/MoneyMgr/releases/download/latest/moneymgr_mobile_arm64-v8a.apk"
)]
pub apk_download_url: String,
} }
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -140,9 +148,17 @@ impl AppConfig {
&ARGS &ARGS
} }
/// Get auto login email (if not empty)
pub fn unsecure_auto_login_email(&self) -> Option<&str> {
match self.unsecure_auto_login_email.as_deref() {
None | Some("") => None,
s => s,
}
}
/// Check if auth is disabled /// Check if auth is disabled
pub fn is_auth_disabled(&self) -> bool { pub fn is_auth_disabled(&self) -> bool {
self.unsecure_auto_login_email.is_some() self.unsecure_auto_login_email().is_some()
} }
/// Get auth cookie domain /// Get auth cookie domain

View File

@@ -70,6 +70,7 @@ impl Default for ServerConstraints {
struct ServerConfig { struct ServerConfig {
auth_disabled: bool, auth_disabled: bool,
oidc_provider_name: &'static str, oidc_provider_name: &'static str,
apk_download_url: &'static str,
accounts_types: &'static [AccountTypeDesc], accounts_types: &'static [AccountTypeDesc],
constraints: ServerConstraints, constraints: ServerConstraints,
} }
@@ -79,6 +80,7 @@ impl Default for ServerConfig {
Self { Self {
auth_disabled: AppConfig::get().is_auth_disabled(), auth_disabled: AppConfig::get().is_auth_disabled(),
oidc_provider_name: AppConfig::get().openid_provider().name, oidc_provider_name: AppConfig::get().openid_provider().name,
apk_download_url: AppConfig::get().apk_download_url.as_str(),
constraints: Default::default(), constraints: Default::default(),
accounts_types: &ACCOUNT_TYPES, accounts_types: &ACCOUNT_TYPES,
} }

View File

@@ -56,7 +56,7 @@ impl FromRequest for AuthExtractor {
}; };
Box::pin(async move { Box::pin(async move {
// Check for authentication using OpenID // Check for authentication using API token
if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) { if let Some(token) = req.headers().get(constants::API_TOKEN_HEADER) {
let Ok(jwt_token) = token.to_str() else { let Ok(jwt_token) = token.to_str() else {
return Err(actix_web::error::ErrorBadRequest( return Err(actix_web::error::ErrorBadRequest(
@@ -182,7 +182,7 @@ impl FromRequest for AuthExtractor {
} }
// Check if login is hard-coded as program argument // Check if login is hard-coded as program argument
if let Some(email) = &AppConfig::get().unsecure_auto_login_email { if let Some(email) = &AppConfig::get().unsecure_auto_login_email() {
let user = users_service::get_user_by_email(email).map_err(|e| { let user = users_service::get_user_by_email(email).map_err(|e| {
log::error!("Failed to retrieve dev user: {e}"); log::error!("Failed to retrieve dev user: {e}");
ErrorPreconditionFailed("Unable to retrieve dev user!") ErrorPreconditionFailed("Unable to retrieve dev user!")

View File

@@ -38,7 +38,7 @@ async fn main() -> std::io::Result<()> {
db_connection::initialize_conn().expect("Failed to connect to PostgresSQL database!"); db_connection::initialize_conn().expect("Failed to connect to PostgresSQL database!");
// Auto create default account, if requested // Auto create default account, if requested
if let Some(mail) = &AppConfig::get().unsecure_auto_login_email { if let Some(mail) = &AppConfig::get().unsecure_auto_login_email() {
users_service::create_or_update_user(mail, "Anonymous") users_service::create_or_update_user(mail, "Anonymous")
.await .await
.expect("Failed to create default account!"); .expect("Failed to create default account!");

View File

@@ -1,3 +1,5 @@
publish_key.properties
gradle-wrapper.jar gradle-wrapper.jar
/.gradle /.gradle
/captures/ /captures/

View File

@@ -0,0 +1,8 @@
# Android version of application
Generate keystore:
```bash
keytool -genkey -v -keystore ./keystore.jks -keyalg RSA \
-keysize 2048 -validity 20000 -alias moneymgr
```

View File

@@ -1,3 +1,6 @@
import java.util.Properties
import java.io.FileInputStream
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@@ -5,6 +8,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("publish_key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android { android {
namespace = "org.communiquons.moneymgr" namespace = "org.communiquons.moneymgr"
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
@@ -21,7 +30,6 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "org.communiquons.moneymgr" applicationId = "org.communiquons.moneymgr"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
@@ -31,13 +39,34 @@ android {
versionName = flutter.versionName versionName = flutter.versionName
} }
buildTypes { signingConfigs {
release { create("publish") {
// TODO: Add your own signing config for the release build. keyAlias = keystoreProperties["keyAlias"] as String
// Signing with the debug keys for now, so `flutter run --release` works. keyPassword = keystoreProperties["keyPassword"] as String
signingConfig = signingConfigs.getByName("debug") storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
} }
} }
buildTypes {
release {
// signingConfig = signingConfigs.getByName("debug")
}
}
flavorDimensions += "default"
productFlavors {
create("development") {
dimension = "default"
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
create("publish") {
dimension = "default"
signingConfig = signingConfigs.getByName("publish")
}
}
} }
flutter { flutter {

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
@@ -6,5 +7,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<!-- In debug mode, unsecure traffic is permitted --> <!-- In debug mode, unsecure traffic is permitted -->
<application android:usesCleartextTraffic="true" /> <application
android:label="MoneyMgr Debug"
android:usesCleartextTraffic="true"
tools:replace="android:label" />
</manifest> </manifest>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false" />
</manifest>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
<exclude domain="external" />
</device-transfer>
</data-extraction-rules>

View File

@@ -0,0 +1,23 @@
#!/bin/bash
SCRIPT_DIR="$(temp=$( realpath "$0" ) && dirname "$temp")"
KEYSTORE_PATH="$SCRIPT_DIR/keystore.jks"
PROPERTIES_PATH="$SCRIPT_DIR/publish_key.properties"
echo Keystore path : $KEYSTORE_PATH
echo Properties path : $PROPERTIES_PATH
[ ! -n "$JKS_KEYSTORE" ] && echo 'Missing JKS_KEYSTORE variable!'&& exit 1
[ ! -n "$JKS_KEYSTORE_PASSWORD" ] && echo 'Missing JKS_KEYSTORE_PASSWORD variable!' && exit 1
# Write keystore
echo $JKS_KEYSTORE | base64 -d > "$KEYSTORE_PATH"
# Write keystore config
cat > "$PROPERTIES_PATH" <<_EOF
storePassword=$JKS_KEYSTORE_PASSWORD
keyPassword=$JKS_KEYSTORE_PASSWORD
keyAlias=moneymgr
storeFile=$KEYSTORE_PATH
_EOF

View File

@@ -0,0 +1,4 @@
storePassword=<password-from-previous-step>
keyPassword=<password-from-previous-step>
keyAlias=upload
storeFile=<keystore-file-location>

View File

@@ -1,39 +1,115 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/ocr_utils.dart';
import 'package:moneymgr_mobile/utils/pdf_utils.dart';
import 'package:moneymgr_mobile/widgets/expense_editor.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart';
part 'scan_screen.g.dart'; part 'scan_screen.g.dart';
/// Scan a document & return generated PDF as byte file
@riverpod @riverpod
Future<String> _scanDocument(Ref ref) async { Future<(Uint8List?, BaseExpenseInfo?)> _scanDocument(Ref ref) async {
var configuration = DocumentScanningFlow( final prefs = ref.watch(prefsProvider).requireValue;
appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK,
),
cleanScanningSession: true,
outputSettings: DocumentScannerOutputSettings(pagesScanLimit: 1),
screens: DocumentScannerScreens(
review: ReviewScreenConfiguration(enabled: false),
),
);
var documentResult = await ScanbotSdkUiV2.startDocumentScanner(configuration);
print("@@@");
print(documentResult);
print("####");
return "changeme"; final pdf = await scanDocAsPDF();
final img = await renderPdf(pdfBytes: pdf);
final amount = await extractInfoFromBill(
imgBuff: img,
extractDates: !prefs.disableExtractDates(),
);
return (pdf, amount);
} }
class ScanScreen extends HookConsumerWidget { class ScanScreen extends HookConsumerWidget {
const ScanScreen({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final boredSuggestion = ref.watch(_scanDocumentProvider); final scanDocProvider = ref.watch(_scanDocumentProvider);
// Perform a switch-case on the result to handle loading/error states final expenses = ref.watch(expensesProvider).requireValue;
return switch (boredSuggestion) {
AsyncData(:final value) => Text('data: $value'), restartScan() async {
AsyncError(:final error) => Text('error: $error'), try {
_ => const Center(child: CircularProgressIndicator()), ref.invalidate(_scanDocumentProvider);
}; Logger.root.info("Load again startup");
} catch (e, s) {
Logger.root.shout("Failed to try again startup loading! $e $s");
}
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: switch (scanDocProvider) {
AsyncData(:final value) when value.$1 != null => ExpenseEditor(
file: value.$1!,
initialData: value.$2,
onFinished: (expense) async {
await expenses.add(
info: expense,
fileContent: value.$1!,
fileMimeType: "application/pdf",
);
if (context.mounted) {
context.pushReplacement(scansPage);
}
},
onRescan: restartScan,
),
// No data
AsyncData(:final value) when value.$1 == null => ScanErrorScreen(
message: "No document scanned!",
onTryAgain: restartScan,
),
// Error
AsyncError(:final error) => ScanErrorScreen(
message: error.toString(),
onTryAgain: restartScan,
),
_ => const Center(child: CircularProgressIndicator()),
},
);
}
}
class ScanErrorScreen extends StatelessWidget {
final String message;
final Function() onTryAgain;
const ScanErrorScreen({
super.key,
required this.message,
required this.onTryAgain,
});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Spacer(flex: 5),
Text("An error occurred while scanning"),
Spacer(flex: 1),
Text(message, textAlign: TextAlign.center),
Spacer(flex: 1),
MaterialButton(
onPressed: onTryAgain,
child: Text("Try again".toUpperCase()),
),
Spacer(flex: 5),
],
),
);
} }
} }

View File

@@ -0,0 +1,86 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/widgets/expense_editor.dart';
import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'scan_details.g.dart';
@riverpod
Future<(Expense, Uint8List)?> _getExpense(Ref ref, {required int id}) async {
final expProvider = ref.watch(expensesProvider).requireValue;
final expense = await expProvider.getById(id);
if (expense == null) return null;
final file = await expProvider.loadFile(expense);
return (expense, file);
}
class ScanDetailScreen extends HookConsumerWidget {
final int id;
const ScanDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final expenses = ref.watch(expensesProvider).requireValue;
final expense = ref.watch(_getExpenseProvider(id: id));
handleUpdate(BaseExpenseInfo newInfo) async {
try {
await expenses.updateExpense(expense.requireValue!.$1, newInfo);
if (context.mounted) {
context.pop();
}
ref.invalidate(expensesProvider);
} catch (e, s) {
Logger.root.warning("Failed to update expense! $e$s");
if (context.mounted) {
context.showTextSnackBar("Failed to update expense! $e");
}
}
}
handleDelete() async {
try {
await expenses.deleteExpense(expense.requireValue!.$1);
if (context.mounted) {
context.pop();
}
ref.invalidate(expensesProvider);
} catch (e, s) {
Logger.root.warning("Failed to delete expense! $e$s");
if (context.mounted) {
context.showTextSnackBar("Failed to delete expense! $e");
}
}
}
return switch (expense) {
AsyncData(:final value) when value == null => FullScreenError(
message: "Expense does not exists!",
error: 'NONE',
),
AsyncData(:final value) => ExpenseEditor(
file: value!.$2,
initialData: value.$1.baseExpense,
onFinished: handleUpdate,
onDelete: handleDelete,
),
AsyncError(:final error) => FullScreenError(
message: "Failed to load expense information!",
error: error.toString(),
),
_ => LoadingScaffold(title: "Expense $id"),
};
}
}

View File

@@ -0,0 +1,76 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/time_utils.dart';
import 'package:moneymgr_mobile/widgets/full_screen_error.dart';
import 'package:moneymgr_mobile/widgets/loading_scaffold.dart';
import 'package:moneymgr_mobile/widgets/synchronize_button.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'scans_list_screen.g.dart';
@riverpod
Future<ExpensesList> _expensesList(Ref ref) {
final expenses = ref.watch(expensesProvider).requireValue;
return expenses.getList();
}
class ScansListScreen extends HookConsumerWidget {
const ScansListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final expensesList = ref.watch(_expensesListProvider);
return switch (expensesList) {
AsyncData(:final value) => Scaffold(
appBar: AppBar(
title: Text("Expenses"),
actions: [value.isEmpty ? Container() : SynchronizeButton()],
),
body: _ExpensesList(list: value),
),
AsyncError(:final error) => FullScreenError(
message: "Failed to load the list of expenses",
error: error.toString(),
),
_ => const LoadingScaffold(title: "Expenses"),
};
}
}
class _ExpensesList extends StatelessWidget {
final ExpensesList list;
const _ExpensesList({required this.list});
@override
Widget build(BuildContext context) {
if (list.isEmpty) {
return const Center(
child: Text("There is no entry waiting for upload (yet)"),
);
}
return ListView.builder(
itemBuilder: (context, entryNum) {
final expense = list[entryNum];
return ListTile(
onTap: () => context.push("$scansPage/${expense.id}"),
leading: Icon(Icons.receipt_long),
title: Text(
expense.label ?? "No label",
style: TextStyle(
fontStyle: expense.label == null ? FontStyle.italic : null,
),
),
subtitle: Text(expense.dateTime.simpleDate),
trailing: Text("${expense.cost}", style: TextStyle(fontSize: 20)),
);
},
itemCount: list.length,
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/providers/settings.dart'; import 'package:moneymgr_mobile/providers/settings.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/extensions.dart'; import 'package:moneymgr_mobile/utils/extensions.dart';
class SettingsScreen extends ConsumerWidget { class SettingsScreen extends ConsumerWidget {
@@ -8,6 +9,7 @@ class SettingsScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(prefsProvider).requireValue;
final themeMode = ref.watch(currentThemeModeProvider); final themeMode = ref.watch(currentThemeModeProvider);
void onTapThemeMode() => void onTapThemeMode() =>
@@ -15,6 +17,16 @@ class SettingsScreen extends ConsumerWidget {
void onTapLicenses() => context.showAppLicensePage(); void onTapLicenses() => context.showAppLicensePage();
handleToggleStartScreen(v) async {
await prefs.setStartOnScansListScreen(v);
ref.invalidate(prefsProvider);
}
handleToggleDisableExtractDate(v) async {
await prefs.setDisableExtractDates(v);
ref.invalidate(prefsProvider);
}
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(title: const Text('Settings')),
body: ListView( body: ListView(
@@ -25,6 +37,22 @@ class SettingsScreen extends ConsumerWidget {
trailing: Text(themeMode.label), trailing: Text(themeMode.label),
onTap: onTapThemeMode, onTap: onTapThemeMode,
), ),
SwitchListTile(
value: prefs.startOnScansListScreen(),
onChanged: handleToggleStartScreen,
title: Text("Start on scans screen"),
subtitle: Text(
"Do not start camera automatically on application startup",
),
),
SwitchListTile(
value: prefs.disableExtractDates(),
onChanged: handleToggleDisableExtractDate,
title: Text("Do not extract dates"),
subtitle: Text(
"Do not attempt to extract dates from scanned expenses",
),
),
const Divider(), const Divider(),
ListTile( ListTile(
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.info_outline),

View File

@@ -24,7 +24,11 @@ class ApiClient {
: client = Dio(BaseOptions(baseUrl: token.apiUrl)); : client = Dio(BaseOptions(baseUrl: token.apiUrl));
/// Get Dio instance /// Get Dio instance
Future<Response<T>> execute<T>(String uri, {String method = "GET"}) async { Future<Response<T>> execute<T>(
String uri, {
String method = "GET",
Object? data,
}) async {
Logger.root.fine("Request on ${token.apiUrl} - URI $uri"); Logger.root.fine("Request on ${token.apiUrl} - URI $uri");
return client.request( return client.request(
uri, uri,
@@ -32,6 +36,7 @@ class ApiClient {
method: method, method: method,
headers: {apiTokenHeader: _genJWT(method, uri)}, headers: {apiTokenHeader: _genJWT(method, uri)},
), ),
data: data,
); );
} }

View File

@@ -0,0 +1,43 @@
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http_parser/http_parser.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/api/api_client.dart';
part 'files_api.freezed.dart';
part 'files_api.g.dart';
@freezed
abstract class UploadResult with _$UploadResult {
const factory UploadResult({required int id}) = _UploadResult;
factory UploadResult.fromJson(Map<String, dynamic> json) =>
_$UploadResultFromJson(json);
}
extension FilesApi on ApiClient {
/// Upload a file
Future<UploadResult> uploadFile({
required String filename,
required String mimeType,
required Uint8List bytes,
}) async {
final res = await execute(
"/file",
method: "POST",
data: FormData.fromMap({
"file": MultipartFile.fromBytes(
bytes,
filename: filename,
contentType: MediaType.parse(mimeType),
),
}),
);
Logger.root.fine("Successfully uploaded file with response=${res.data}");
return UploadResult.fromJson(res.data);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'api_client.dart';
part 'inbox_api.freezed.dart';
part 'inbox_api.g.dart';
@freezed
abstract class UpdateInboxEntryRequest with _$UpdateInboxEntryRequest {
const factory UpdateInboxEntryRequest({
// ignore: non_constant_identifier_names
required int file_id,
required int time,
required String? label,
required double? amount,
}) = _UpdateInboxEntryRequest;
factory UpdateInboxEntryRequest.fromJson(Map<String, dynamic> json) =>
_$UpdateInboxEntryRequestFromJson(json);
}
extension InboxApi on ApiClient {
/// Create a new inbox entry
Future<void> createInboxEntry(UpdateInboxEntryRequest entry) async {
await execute("/inbox", method: "POST", data: entry.toJson());
}
}

View File

@@ -7,8 +7,11 @@ import 'package:moneymgr_mobile/routes/login/manual_auth_screen.dart';
import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart'; import 'package:moneymgr_mobile/routes/login/qr_auth_screen.dart';
import 'package:moneymgr_mobile/routes/profile/profile_screen.dart'; import 'package:moneymgr_mobile/routes/profile/profile_screen.dart';
import 'package:moneymgr_mobile/routes/scan/scan_screen.dart'; import 'package:moneymgr_mobile/routes/scan/scan_screen.dart';
import 'package:moneymgr_mobile/routes/scan_details/scan_details.dart';
import 'package:moneymgr_mobile/routes/scans_list/scans_list_screen.dart';
import 'package:moneymgr_mobile/routes/settings/settings_screen.dart'; import 'package:moneymgr_mobile/routes/settings/settings_screen.dart';
import 'package:moneymgr_mobile/services/router/routes_list.dart'; import 'package:moneymgr_mobile/services/router/routes_list.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/widgets/load_startup_data.dart'; import 'package:moneymgr_mobile/widgets/load_startup_data.dart';
import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart'; import 'package:moneymgr_mobile/widgets/scaffold_with_navigation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -27,6 +30,8 @@ GoRouter router(Ref ref) {
authStateNotifier.value = value; authStateNotifier.value = value;
}); });
final prefs = ref.read(prefsProvider).requireValue;
// This is the only place you need to define your navigation items. The items // This is the only place you need to define your navigation items. The items
// will be propagated automatically to the router and the navigation bar/rail // will be propagated automatically to the router and the navigation bar/rail
// of the scaffold. // of the scaffold.
@@ -35,12 +40,28 @@ GoRouter router(Ref ref) {
// see [AuthState] enum. // see [AuthState] enum.
final navigationItems = [ final navigationItems = [
NavigationItem( NavigationItem(
path: scanPage, path: capturePage,
body: (_) => ScanScreen(), body: (_) => ScanScreen(),
icon: Icons.camera_alt_outlined, icon: Icons.camera_alt_outlined,
selectedIcon: Icons.camera_alt, selectedIcon: Icons.camera_alt,
label: "Scan", label: "Scan",
), ),
NavigationItem(
path: scansPage,
body: (_) => ScansListScreen(),
icon: Icons.list,
selectedIcon: Icons.list_alt,
label: "List",
routes: [
GoRoute(
path: ":id",
builder: (_, state) {
final id = int.parse(state.pathParameters["id"]!);
return ScanDetailScreen(id: id);
},
),
],
),
NavigationItem( NavigationItem(
path: profilePage, path: profilePage,
body: (_) => ProfileScreen(), body: (_) => ProfileScreen(),
@@ -52,22 +73,22 @@ GoRouter router(Ref ref) {
final router = GoRouter( final router = GoRouter(
debugLogDiagnostics: true, debugLogDiagnostics: true,
initialLocation: navigationItems.first.path, initialLocation: prefs.startOnScansListScreen() ? scansPage : capturePage,
routes: [ routes: [
GoRoute(path: homePage, builder: (_, __) => const Scaffold()), GoRoute(path: homePage, builder: (_, _) => const Scaffold()),
GoRoute(path: authPage, builder: (_, __) => const LoginScreen()), GoRoute(path: authPage, builder: (_, _) => const LoginScreen()),
GoRoute(path: qrAuthPath, builder: (_, __) => const QrAuthScreen()), GoRoute(path: qrAuthPath, builder: (_, _) => const QrAuthScreen()),
GoRoute( GoRoute(
path: manualAuthPage, path: manualAuthPage,
builder: (_, __) => const ManualAuthScreen(), builder: (_, _) => const ManualAuthScreen(),
), ),
GoRoute(path: settingsPage, builder: (_, __) => const SettingsScreen()), GoRoute(path: settingsPage, builder: (_, _) => const SettingsScreen()),
// Configuration for the bottom navigation bar routes. The routes themselves // Configuration for the bottom navigation bar routes. The routes themselves
// should be defined in [navigationItems]. Modification to this [ShellRoute] // should be defined in [navigationItems]. Modification to this [ShellRoute]
// config is rarely needed. // config is rarely needed.
ShellRoute( ShellRoute(
builder: (_, __, child) => child, builder: (_, _, child) => child,
routes: [ routes: [
for (final (index, item) in navigationItems.indexed) for (final (index, item) in navigationItems.indexed)
GoRoute( GoRoute(

View File

@@ -13,8 +13,11 @@ const manualAuthPage = "/login/manual";
/// Settings path /// Settings path
const settingsPage = "/settings"; const settingsPage = "/settings";
/// Scan URL path /// Scan path
const scanPage = "/scan"; const capturePage = "/scan";
/// Scans page
const scansPage = "/scans";
/// Profile path /// Profile path
const profilePage = "/profile"; const profilePage = "/profile";

View File

@@ -0,0 +1,183 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'expenses.freezed.dart';
part 'expenses.g.dart';
typedef ExpensesList = List<Expense>;
@freezed
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
const BaseExpenseInfo._();
const factory BaseExpenseInfo({
required String? label,
required double cost,
required DateTime time,
}) = _BaseExpenseInfo;
/// Get expense time as second
int get timeAsSeconds => (time.millisecondsSinceEpoch / 1000).floor();
}
@freezed
abstract class Expense with _$Expense {
const Expense._();
const factory Expense({
/// Internal id used to identify the expense
required int id,
/// Label of the expense
required String? label,
/// The cost shall always be a positive value
required double cost,
/// Time associated with the expense (seconds since epoch)
required int time,
/// Associated file mime type
required String mimeType,
}) = _Expense;
factory Expense.fromJson(Map<String, dynamic> json) =>
_$ExpenseFromJson(json);
/// Get base expense information
BaseExpenseInfo get baseExpense =>
BaseExpenseInfo(label: label, cost: cost, time: dateTime);
/// Get associated expense file name
String get localFileName {
if (mimeType == "application/pdf") return "$id.pdf";
if (mimeType == "image/jpeg") return "$id.jpeg";
if (mimeType == "image/png") return "$id.png";
return id.toString();
}
/// Get expense date
DateTime get dateTime => DateTime.fromMillisecondsSinceEpoch(time * 1000);
}
@riverpod
Future<ExpensesManager> expenses(Ref ref) async => ExpensesManager.instance();
class ExpensesManager {
final String storagePath;
ExpensesManager._({required this.storagePath});
/// Get an instance of this manager
static Future<ExpensesManager> instance() async {
final appDir = await getApplicationDocumentsDirectory();
final subDir = p.join(appDir.absolute.path, "expenses");
final result = await Directory(subDir).create(recursive: true);
return ExpensesManager._(storagePath: result.absolute.path);
}
/// Get expenses list file path
File get expenseFile => File(p.join(storagePath, "list.json"));
/// Get the files storage path
Directory get filesStoragePath => Directory(p.join(storagePath, "exp_files"));
/// Get the current list of expenses
Future<ExpensesList> getList() async {
// On first save the list does not exists.
if (!await expenseFile.exists()) {
return [];
}
final jsonDec = jsonDecode(await expenseFile.readAsString());
return List<Expense>.from(jsonDec.map((m) => Expense.fromJson(m)));
}
/// Save the list of expenses
Future<void> saveList(ExpensesList list) async {
final jsonDoc = jsonEncode(list.map((t) => t.toJson()).toList());
await expenseFile.writeAsString(jsonDoc);
}
/// Add a new expense to the list
Future<void> add({
required BaseExpenseInfo info,
required List<int> fileContent,
required String fileMimeType,
}) async {
final list = await getList();
final exp = Expense(
id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000),
label: info.label,
cost: info.cost,
time: info.timeAsSeconds,
mimeType: fileMimeType,
);
// Create files storage directory if required
if (!await filesStoragePath.exists()) {
await filesStoragePath.create(recursive: true);
}
// Save associated file
final file = File(
p.join(filesStoragePath.absolute.path, exp.localFileName),
);
await file.writeAsBytes(fileContent);
// Save the list of expenses
list.add(exp);
await saveList(list);
}
/// Get a single expense information by its ID
Future<Expense?> getById(int id) async {
final list = await getList();
return list.firstWhere((e) => e.id == id);
}
/// Load the file associated with an expense
Future<Uint8List> loadFile(Expense expense) async {
final path = p.join(filesStoragePath.absolute.path, expense.localFileName);
return File(path).readAsBytes();
}
/// Update expense information
Future<void> updateExpense(Expense expense, BaseExpenseInfo newInfo) async {
final list = await getList();
final entry = list.indexWhere((e) => e.id == expense.id);
list[entry] = Expense(
id: expense.id,
label: newInfo.label,
cost: newInfo.cost,
time: newInfo.timeAsSeconds,
mimeType: expense.mimeType,
);
saveList(list);
}
/// Delete an expense
Future<void> deleteExpense(Expense expense) async {
// Remove expense from the list
final list = await getList();
await saveList(list.where((e) => e.id != expense.id).toList());
// Delete associated file, if any
final filePath = File(
p.join(filesStoragePath.absolute.path, expense.localFileName),
);
if (await filePath.exists()) {
await filePath.delete();
}
}
}

View File

@@ -15,6 +15,22 @@ Future<SharedPreferencesWithCache> prefs(Ref ref) =>
); );
extension MoneyMgrSharedPreferences on SharedPreferencesWithCache { extension MoneyMgrSharedPreferences on SharedPreferencesWithCache {
bool startOnScansListScreen() {
return getBool("startOnScansListScreen") ?? false;
}
Future<void> setStartOnScansListScreen(bool start) async {
await setBool("startOnScansListScreen", start);
}
bool disableExtractDates() {
return getBool("disableExtractDates") ?? false;
}
Future<void> setDisableExtractDates(bool disable) async {
await setBool("disableExtractDates", disable);
}
ServerConfig? serverConfig() { ServerConfig? serverConfig() {
final json = getString("serverConfig"); final json = getString("serverConfig");
if (json != null) return ServerConfig.fromJson(jsonDecode(json)); if (json != null) return ServerConfig.fromJson(jsonDecode(json));

View File

@@ -0,0 +1,86 @@
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
/// Attempt to extract information from invoice image
Future<BaseExpenseInfo?> extractInfoFromBill({
required Uint8List imgBuff,
required bool extractDates,
}) async {
final decodedImage = await decodeImageFromList(imgBuff);
final byteData = await decodedImage.toByteData(
format: ui.ImageByteFormat.rawRgba,
);
final image = InputImage.fromBitmap(
bitmap: byteData!.buffer.asUint8List(),
width: decodedImage.width,
height: decodedImage.height,
);
final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
final extractionResult = await textRecognizer.processImage(image);
Logger.root.fine("Expense text: ${extractionResult.text}");
// Check for highestCost amount on invoice
final costRegexp = RegExp(
r'([0-9]+([ ]*(\\.|,)[ ]*[0-9]{1,2}){0,1})([ \\t\\n]*(EUR|eur|€)|E)',
multiLine: true,
caseSensitive: false,
);
var highestCost = 0.0;
for (final match in costRegexp.allMatches(extractionResult.text)) {
if (match.groupCount == 0) continue;
// Process only numeric value
final value = (match.group(1) ?? "").replaceAll(",", ".");
highestCost = max(highestCost, double.tryParse(value) ?? 0.0);
}
// Check for highestCost amount on invoice
final dateRegexp = RegExp(
r'([0-3][0-9])(\/|-)([0-1][0-9])(\/|-)((20|)[0-9]{2})',
multiLine: false,
caseSensitive: false,
);
final currDate = DateTime.now();
DateTime? newest;
for (final match in dateRegexp.allMatches(extractionResult.text)) {
if (match.groupCount < 6) continue;
int year = int.tryParse(match.group(5)!) ?? currDate.year;
try {
final date = DateTime(
year > 99 ? year : (2000 + year),
int.tryParse(match.group(3)!) ?? currDate.month,
int.tryParse(match.group(1)!) ?? currDate.day,
);
if (newest == null) {
newest = date;
} else {
newest = DateTime.fromMillisecondsSinceEpoch(
max(newest.millisecondsSinceEpoch, date.millisecondsSinceEpoch),
);
}
} catch (e, s) {
Logger.root.warning("Failed to parse date! $e$s");
}
}
return BaseExpenseInfo(
label: null,
cost: highestCost,
time: extractDates && (newest?.isBefore(currDate) ?? false)
? newest!
: currDate,
);
}

View File

@@ -0,0 +1,91 @@
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:pdf_image_renderer/pdf_image_renderer.dart';
import 'package:scanbot_sdk/scanbot_sdk.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
/// Scan document as PDF
Future<Uint8List> scanDocAsPDF() async {
var configuration = DocumentScanningFlow(
appearance: DocumentFlowAppearanceConfiguration(
statusBarMode: StatusBarMode.DARK,
),
cleanScanningSession: true,
outputSettings: DocumentScannerOutputSettings(pagesScanLimit: 1),
screens: DocumentScannerScreens(
review: ReviewScreenConfiguration(enabled: false),
),
);
var documentResult = await ScanbotSdkUiV2.startDocumentScanner(configuration);
if (documentResult.status != OperationStatus.OK) {
throw Exception("Scanner failed with status ${documentResult.status}");
}
// Convert result to PDF
var result = await ScanbotSdk.document.createPDFForDocument(
PDFFromDocumentParams(
documentID: documentResult.data!.uuid,
pdfConfiguration: PdfConfiguration(),
),
);
final pdfPath = result.pdfFileUri.replaceFirst("file://", "");
return File(pdfPath).readAsBytes();
}
/// Render PDF to image bits
Future<Uint8List> renderPdf({String? path, Uint8List? pdfBytes}) async {
assert(path != null || pdfBytes != null);
// Create temporary file if required
var isTemp = false;
if (path == null) {
path = p.join(
(await getTemporaryDirectory()).absolute.path,
"render-${Random().nextInt(10000).toString()}+.pdf",
);
await File(path).writeAsBytes(pdfBytes!);
isTemp = true;
}
try {
final pdf = PdfImageRenderer(path: path);
await pdf.open();
await pdf.openPage(pageIndex: 0);
// get the render size after the page is loaded
final size = await pdf.getPageSize(pageIndex: 0);
// get the actual image of the page
final img = await pdf.renderPage(
pageIndex: 0,
x: 0,
y: 0,
width: size.width,
// you can pass a custom size here to crop the image
height: size.height,
// you can pass a custom size here to crop the image
scale: 1,
// increase the scale for better quality (e.g. for zooming)
background: Colors.white,
);
// close the page again
await pdf.closePage(pageIndex: 0);
// close the PDF after rendering the page
pdf.close();
return img!;
} finally {
if (isTemp) {
await File(path).delete();
}
}
}

View File

@@ -1,3 +1,8 @@
int secondsSinceEpoch(DateTime time) { int secondsSinceEpoch(DateTime time) {
return time.millisecondsSinceEpoch ~/ 1000; return time.millisecondsSinceEpoch ~/ 1000;
} }
extension SimpleDateFormatting on DateTime {
String get simpleDate =>
"${day.toString().padLeft(2, "0")}/${month.toString().padLeft(2, '0')}/$year";
}

View File

@@ -0,0 +1,213 @@
import 'dart:typed_data';
import 'package:confirm_dialog/confirm_dialog.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/time_utils.dart';
import 'package:moneymgr_mobile/widgets/pdf_viewer.dart';
import '../utils/hooks.dart';
class ExpenseEditor extends HookConsumerWidget {
final Uint8List file;
final Future<void> Function(BaseExpenseInfo) onFinished;
final Function()? onRescan;
final Function()? onDelete;
final BaseExpenseInfo? initialData;
const ExpenseEditor({
super.key,
required this.file,
required this.onFinished,
this.onRescan,
this.onDelete,
this.initialData,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
final labelController = useTextEditingController(text: initialData?.label);
final costController = useTextEditingController(
text: initialData?.cost.toString(),
);
final timeController = useState(initialData?.time ?? DateTime.now());
final (:pending, :snapshot, :hasError) = useAsyncTask();
// Force refresh of field if required
final previousData = useState<BaseExpenseInfo?>(null);
if (initialData != previousData.value) {
previousData.value = initialData;
labelController.text = initialData?.label ?? "";
costController.text = initialData?.cost.toString() ?? "";
timeController.value = initialData?.time ?? DateTime.now();
}
// Clear cost value
handleClearCost() {
costController.text = "";
}
// Pick a new date
handlePickDate() async {
final date = await showDatePicker(
context: context,
firstDate: DateTime(2000),
lastDate: DateTime(2099),
initialDate: timeController.value,
);
if (date != null) timeController.value = date;
}
// Save expense
handleSubmit() async {
if (costController.text.isEmpty) {
context.showTextSnackBar("Please specify expense cost!");
return;
}
pending.value = onFinished(
BaseExpenseInfo(
label: labelController.text.isEmpty ? null : labelController.text,
cost: double.tryParse(costController.text) ?? 0,
time: timeController.value,
),
);
// Reset screen after a scan
await pending.value;
labelController.text = "";
costController.text = "";
}
// Cancel operation
handleRescan() async {
if (await confirm(
context,
content: Text("Do you really want to discard this expense?"),
) &&
onRescan != null) {
onRescan!();
}
}
// Delete expense
handleDelete() async {
if (await confirm(
context,
content: Text("Do you really want to delete this expense?"),
) &&
onDelete != null) {
onDelete!();
}
}
// Open invoice in full screen
handleFullScreenInvoice() {
showDialog(
context: context,
builder: (c) => Scaffold(
appBar: AppBar(title: Text("Expense")),
body: SingleChildScrollView(
child: PDFViewer(pdfBytes: file, fit: BoxFit.fitWidth),
),
),
);
}
return Scaffold(
appBar: AppBar(
title: Text("Expense info"),
actions: [
// Rescan expense
onRescan == null
? Container()
: IconButton(
onPressed: handleRescan,
icon: Icon(Icons.restart_alt),
),
// Delete expense
onDelete == null
? Container()
: IconButton(onPressed: handleDelete, icon: Icon(Icons.delete)),
// Submit
snapshot.connectionState == ConnectionState.waiting
? CircularProgressIndicator()
: IconButton(
onPressed: handleSubmit,
icon: Icon(Icons.save),
color: hasError ? Colors.red : null,
),
],
),
body: Column(
children: [
// Expense preview
Expanded(
child: GestureDetector(
onTap: handleFullScreenInvoice,
child: PDFViewer(pdfBytes: file, fit: BoxFit.contain),
),
),
SizedBox(height: 10),
// Cost
TextField(
controller: costController,
keyboardType: TextInputType.numberWithOptions(
decimal: true,
signed: false,
),
decoration: InputDecoration(
labelText: 'Cost',
suffixIcon: IconButton(
onPressed: handleClearCost,
icon: const Icon(Icons.clear),
),
),
textInputAction: TextInputAction.done,
),
SizedBox(height: 10),
// Date
TextField(
enabled: true,
readOnly: true,
controller: TextEditingController(
text: timeController.value.simpleDate,
),
keyboardType: TextInputType.datetime,
decoration: InputDecoration(
labelText: 'Date',
suffixIcon: IconButton(
onPressed: handlePickDate,
icon: const Icon(Icons.date_range),
),
),
),
SizedBox(height: 10),
// Label
TextField(
controller: labelController,
keyboardType: TextInputType.text,
decoration: const InputDecoration(labelText: 'Label'),
textInputAction: TextInputAction.done,
maxLength: serverConfig.constraints.inbox_entry_label.max,
),
],
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class LoadingScaffold extends StatelessWidget {
final String title;
const LoadingScaffold({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(child: CircularProgressIndicator()),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/utils/pdf_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'pdf_viewer.g.dart';
@riverpod
Future<Uint8List> _renderPdf(
Ref ref, {
String? path,
Uint8List? pdfBytes,
}) async {
return renderPdf(path: path, pdfBytes: pdfBytes);
}
class PDFViewer extends ConsumerWidget {
final String? pdfPath;
final Uint8List? pdfBytes;
final double? width;
final double? height;
final BoxFit? fit;
const PDFViewer({
super.key,
this.pdfPath,
this.pdfBytes,
this.width,
this.height,
this.fit,
}) : assert(pdfPath != null || pdfBytes != null);
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(
_renderPdfProvider(path: pdfPath, pdfBytes: pdfBytes),
);
// Perform a switch-case on the result to handle loading/error states
return switch (provider) {
AsyncData(:final value) => Image(
image: MemoryImage(value),
width: width,
height: height,
fit: fit,
),
AsyncError(:final error) => Text('PDF error: $error'),
_ => Center(child: const CircularProgressIndicator()),
};
}
}

View File

@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:moneymgr_mobile/services/api/api_client.dart';
import 'package:moneymgr_mobile/services/api/files_api.dart';
import 'package:moneymgr_mobile/services/api/inbox_api.dart';
import 'package:moneymgr_mobile/services/storage/expenses.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
import 'package:moneymgr_mobile/utils/hooks.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'synchronize_button.g.dart';
/// Synchronize expenses list with backend
@riverpod
Future<void> _performSynchronization(Ref ref) async {
final expenses = ref.watch(expensesProvider).requireValue;
final apiService = ref.watch(apiServiceProvider)!;
final list = await expenses.getList();
for (final exp in list) {
// First, upload file
final bytes = await expenses.loadFile(exp);
final file = await apiService.uploadFile(
filename: exp.localFileName,
mimeType: exp.mimeType,
bytes: bytes,
);
// Then, create the inbox entry
await apiService.createInboxEntry(
UpdateInboxEntryRequest(
file_id: file.id,
time: exp.time,
label: exp.label,
amount: -1 * exp.cost,
),
);
// Lastly delete the local expense
ref.watch(expensesProvider).requireValue.deleteExpense(exp);
}
ref.invalidate(expensesProvider);
}
class SynchronizeButton extends HookConsumerWidget {
const SynchronizeButton({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final (:pending, :snapshot, :hasError) = useAsyncTask();
handleSynchronize() async {
try {
await ref.watch(
_performSynchronizationProvider.selectAsync((it) => it),
);
} catch (e, s) {
Logger.root.warning("Failed to synchronize expenses! $e $s");
if (context.mounted) {
context.showTextSnackBar("Failed to synchronize expenses! $e");
}
}
}
return snapshot.connectionState == ConnectionState.waiting
? Padding(
padding: const EdgeInsets.all(12.0),
child: SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: IconButton(
onPressed: () => pending.value = handleSynchronize(),
style: ButtonStyle(
backgroundColor: hasError
? WidgetStatePropertyAll(Colors.red)
: null,
),
icon: Icon(Icons.sync_rounded),
);
}
}

View File

@@ -141,10 +141,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.10.1" version: "8.11.0"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -370,10 +370,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "6.0.0"
flutter_native_splash: flutter_native_splash:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -452,18 +452,18 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: freezed name: freezed
sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c" sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.6" version: "3.1.0"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.1.0"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@@ -484,10 +484,26 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: go_router name: go_router
sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.2.4" version: "16.0.0"
google_mlkit_commons:
dependency: transitive
description:
name: google_mlkit_commons
sha256: "8f40fbac10685cad4715d11e6a0d86837d9ad7168684dfcad29610282a88e67a"
url: "https://pub.dev"
source: hosted
version: "0.11.0"
google_mlkit_text_recognition:
dependency: "direct main"
description:
name: google_mlkit_text_recognition
sha256: "96173ad4dd7fd06c660e22ac3f9e9f1798a517fe7e48bee68eeec83853224224"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@@ -529,7 +545,7 @@ packages:
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
http_parser: http_parser:
dependency: transitive dependency: "direct main"
description: description:
name: http_parser name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
@@ -604,10 +620,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.1" version: "6.0.0"
logging: logging:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -665,7 +681,7 @@ packages:
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -673,7 +689,7 @@ packages:
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_provider: path_provider:
dependency: transitive dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
@@ -720,6 +736,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.0" version: "2.3.0"
pdf_image_renderer:
dependency: "direct main"
description:
name: pdf_image_renderer
sha256: "0ec76118b14663f17f9b6a8c29ec59cb1b82e466a3c16fbb2ed9f1b613fc41b7"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -913,10 +937,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_helper name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.5" version: "1.3.6"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:

View File

@@ -49,10 +49,10 @@ dependencies:
riverpod_annotation: ^2.6.1 riverpod_annotation: ^2.6.1
# Implement React hooks in Flutter # Implement React hooks in Flutter
flutter_hooks: ^0.21.2 flutter_hooks: ^0.21.3+1
# Router # Router
go_router: ^15.2.4 go_router: ^16.0.0
# Flutter extras widgets for columns and rows # Flutter extras widgets for columns and rows
flextras: ^1.0.0 flextras: ^1.0.0
@@ -72,6 +72,7 @@ dependencies:
# API requests # API requests
dio: ^5.8.0+1 dio: ^5.8.0+1
http_parser: ^4.1.2
# Qr Code library # Qr Code library
mobile_scanner: ^7.0.1 mobile_scanner: ^7.0.1
@@ -83,7 +84,17 @@ dependencies:
# Document scanner # Document scanner
# flutter_doc_scanner: ^0.0.16 # no bundled support yet # flutter_doc_scanner: ^0.0.16 # no bundled support yet
# https://developers.google.com/ml-kit/tips/installation-paths # https://developers.google.com/ml-kit/tips/installation-paths
scanbot_sdk: ^7.0.0 scanbot_sdk: ^7.0.1
# Get documents path
path_provider: ^2.1.5
path: ^1.9.1
# PDF renderer
pdf_image_renderer: ^1.0.1
# Text extraction
google_mlkit_text_recognition: ^0.15.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -94,13 +105,13 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^6.0.0
# Manage app icon # Manage app icon
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
# Generate source code # Generate source code
build_runner: ^2.5.4 build_runner: ^2.6.1
# Riverpod code generation # Riverpod code generation
riverpod_generator: ^2.6.5 riverpod_generator: ^2.6.5
@@ -119,6 +130,9 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# Default Android flavor
default-flavor: development
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.

File diff suppressed because it is too large Load Diff

View File

@@ -11,39 +11,39 @@
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.6", "@fontsource/roboto": "^5.2.8",
"@jsonjoy.com/base64": "^1.1.2", "@jsonjoy.com/base64": "^1.1.2",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.1.2", "@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2", "@mui/material": "^7.1.2",
"@mui/x-charts": "^8.5.3", "@mui/x-charts": "^8.10.2",
"@mui/x-data-grid": "^8.5.3", "@mui/x-data-grid": "^8.9.2",
"@mui/x-date-pickers": "^8.5.3", "@mui/x-date-pickers": "^8.9.2",
"date-and-time": "^3.6.0", "date-and-time": "^3.6.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.18",
"filesize": "^10.1.6", "filesize": "^10.1.6",
"qrcode.react": "^4.2.0", "qrcode.react": "^4.2.0",
"react": "^19.1.0", "react": "^19.2.0",
"react-dom": "^19.1.0", "react-dom": "^19.2.0",
"react-router": "^7.6.2", "react-router": "^7.6.3",
"react-router-dom": "^7.6.2", "react-router-dom": "^7.6.3",
"ts-pattern": "^5.7.1" "ts-pattern": "^5.8.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.29.0", "@eslint/js": "^9.33.0",
"@types/react": "^19.1.8", "@types/react": "^19.2.0",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.26.0", "eslint": "^9.32.0",
"eslint-plugin-react-dom": "^1.49.0", "eslint-plugin-react-dom": "^1.52.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^00.4.20", "eslint-plugin-react-refresh": "^00.4.20",
"eslint-plugin-react-x": "^1.52.2", "eslint-plugin-react-x": "^1.52.9",
"globals": "^16.2.0", "globals": "^16.3.0",
"typescript": "~5.8.3", "typescript": "~5.8.3",
"typescript-eslint": "^8.32.1", "typescript-eslint": "^8.32.1",
"vite": "^6.3.5" "vite": "^6.3.6"
} }
} }

View File

@@ -3,6 +3,7 @@ import { APIClient } from "./ApiClient";
export interface ServerConfig { export interface ServerConfig {
auth_disabled: boolean; auth_disabled: boolean;
oidc_provider_name: string; oidc_provider_name: string;
apk_download_url: string;
accounts_types: AccountType[]; accounts_types: AccountType[];
constraints: ServerConstraints; constraints: ServerConstraints;
} }

View File

@@ -2,9 +2,11 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import DriveFileMoveOutlineIcon from "@mui/icons-material/DriveFileMoveOutline"; import DriveFileMoveOutlineIcon from "@mui/icons-material/DriveFileMoveOutline";
import LinkOffIcon from "@mui/icons-material/LinkOff"; import LinkOffIcon from "@mui/icons-material/LinkOff";
import MoreVertIcon from "@mui/icons-material/MoreVert"; import MoreVertIcon from "@mui/icons-material/MoreVert";
import ClearIcon from "@mui/icons-material/Clear";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import { import {
IconButton, IconButton,
InputAdornment,
ListItemIcon, ListItemIcon,
ListItemText, ListItemText,
TextField, TextField,
@@ -392,6 +394,19 @@ function MovementsTable(p: {
setFilter(e.target.value); setFilter(e.target.value);
}} }}
style={{ padding: "0px", flex: 1 }} style={{ padding: "0px", flex: 1 }}
slotProps={{
input: {
endAdornment: filter.length > 0 && (
<InputAdornment position="end">
<Tooltip title="Clear current filter">
<IconButton size="small" onClick={() => { setFilter(""); }}>
<ClearIcon />
</IconButton>
</Tooltip>
</InputAdornment>
),
},
}}
/> />
<span style={{ flex: 1 }}></span> <span style={{ flex: 1 }}></span>
<Tooltip title="Refresh table"> <Tooltip title="Refresh table">

View File

@@ -280,6 +280,8 @@ function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement {
The API token was successfully created. Please note the following The API token was successfully created. Please note the following
information as they won't be available next. information as they won't be available next.
<br /> <br />
API URL : <CopyTextChip text={APIClient.ActualBackendURL()} />
<br />
Token ID: <CopyTextChip text={p.token.id.toString()} /> Token ID: <CopyTextChip text={p.token.id.toString()} />
<br /> <br />
Token value: <CopyTextChip text={p.token.token} /> Token value: <CopyTextChip text={p.token.token} />

View File

@@ -1,5 +1,6 @@
import { mdiApi, mdiCash } from "@mdi/js"; import { mdiApi, mdiCash } from "@mdi/js";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import AndroidIcon from "@mui/icons-material/Android";
import CloudDownloadIcon from "@mui/icons-material/CloudDownload"; import CloudDownloadIcon from "@mui/icons-material/CloudDownload";
import LogoutIcon from "@mui/icons-material/Logout"; import LogoutIcon from "@mui/icons-material/Logout";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
@@ -10,6 +11,7 @@ import MenuItem from "@mui/material/MenuItem";
import Toolbar from "@mui/material/Toolbar"; import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import * as React from "react"; import * as React from "react";
import { ServerApi } from "../api/ServerApi";
import { useAuthInfo } from "./BaseAuthenticatedPage"; import { useAuthInfo } from "./BaseAuthenticatedPage";
import { DarkThemeButton } from "./DarkThemeButtonWidget"; import { DarkThemeButton } from "./DarkThemeButtonWidget";
import { PublicModeButton } from "./PublicModeButtonWidget"; import { PublicModeButton } from "./PublicModeButtonWidget";
@@ -100,6 +102,18 @@ export function MoneyWebAppBar(p: {
</MenuItem> </MenuItem>
</RouterLink> </RouterLink>
{/* APK download */}
<RouterLink to={ServerApi.Config.apk_download_url}>
<MenuItem>
<ListItemIcon>
<AndroidIcon />
</ListItemIcon>
<ListItemText secondary="Scan expenses from your smartphone">
Mobile Application
</ListItemText>
</MenuItem>
</RouterLink>
<Divider /> <Divider />
{/* Sign out */} {/* Sign out */}