175 Commits

Author SHA1 Message Date
2490b5b4f4 Update dependency eslint-plugin-react-dom to ^1.53.1
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2025-10-06 00:15:59 +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
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
39 changed files with 2415 additions and 1593 deletions

View File

@@ -4,9 +4,17 @@ type: docker
name: default
steps:
# Needs a full git clone
- name: fetch
image: alpine/git
commands:
- git fetch --tags
# Frontend
- name: web_build
image: node:23
depends_on:
- fetch
volumes:
- name: web_app
path: /tmp/web_build
@@ -20,6 +28,8 @@ steps:
# Backend
- name: backend_fetch_deps
image: rust
depends_on:
- fetch
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
@@ -54,6 +64,9 @@ steps:
- name: backend_build
image: rust
when:
event:
- tag
volumes:
- name: rust_registry
path: /usr/local/cargo/registry
@@ -72,12 +85,58 @@ steps:
- ls -lah target/release/moneymgr_backend target/release/examples/api_curl
- 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
- name: gitea_release
image: plugins/gitea-release
depends_on:
- backend_build
- mobile_app_build
when:
event:
- tag

View File

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

View File

@@ -5,26 +5,26 @@ edition = "2024"
[dependencies]
env_logger = "0.11.8"
log = "0.4.27"
diesel = { version = "2.2.11", features = ["postgres", "r2d2"] }
log = "0.4.28"
diesel = { version = "2.2.12", features = ["postgres", "r2d2"] }
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-cors = "0.7.1"
actix-multipart = "0.7.2"
actix-remote-ip = "0.1.0"
actix-session = { version = "0.10.1", features = ["redis-session"] }
actix-files = "0.6.6"
actix-files = "0.6.8"
lazy_static = "1.5.0"
anyhow = "1.0.98"
anyhow = "1.0.100"
serde = { version = "1.0.219", features = ["derive"] }
rust-s3 = "0.36.0-beta.2"
thiserror = "2.0.12"
tokio = "1.45.1"
futures-util = "0.3.31"
serde_json = "1.0.140"
serde_json = "1.0.142"
light-openid = "1.0.4"
rand = "0.9.1"
rand = "0.9.2"
ipnet = { version = "2.11.0", features = ["serde"] }
lazy-regex = "3.4.1"
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"
base16ct = "0.2.0"
httpdate = "1.0.3"
chrono = "0.4.41"
chrono = "0.4.42"
tempfile = "3.20.0"
zip = "3.0.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
/// email address by default
#[clap(long, env)]
pub unsecure_auto_login_email: Option<String>,
unsecure_auto_login_email: Option<String>,
/// PostgreSQL database host
#[clap(long, env, default_value = "localhost")]
@@ -126,6 +126,14 @@ pub struct AppConfig {
/// Redis password
#[clap(long, env, default_value = "secretredis")]
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! {
@@ -140,9 +148,17 @@ impl AppConfig {
&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
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

View File

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

View File

@@ -56,7 +56,7 @@ impl FromRequest for AuthExtractor {
};
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) {
let Ok(jwt_token) = token.to_str() else {
return Err(actix_web::error::ErrorBadRequest(
@@ -182,7 +182,7 @@ impl FromRequest for AuthExtractor {
}
// 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| {
log::error!("Failed to retrieve dev user: {e}");
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!");
// 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")
.await
.expect("Failed to create default account!");

View File

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

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 Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
@@ -6,5 +7,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<!-- In debug mode, unsecure traffic is permitted -->
<application android:usesCleartextTraffic="true" />
<application
android:label="MoneyMgr Debug"
android:usesCleartextTraffic="true"
tools:replace="android:label" />
</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,45 +1,31 @@
import 'dart:io';
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/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:scanbot_sdk/scanbot_sdk.dart';
import 'package:scanbot_sdk/scanbot_sdk_ui_v2.dart' hide IconButton, EdgeInsets;
part 'scan_screen.g.dart';
/// Scan a document & return generated PDF as byte file
@riverpod
Future<Uint8List?> _scanDocument(Ref ref) 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);
Future<(Uint8List?, BaseExpenseInfo?)> _scanDocument(Ref ref) async {
final prefs = ref.watch(prefsProvider).requireValue;
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 pdf = await scanDocAsPDF();
final img = await renderPdf(pdfBytes: pdf);
final amount = await extractInfoFromBill(
imgBuff: img,
extractDates: !prefs.disableExtractDates(),
);
final pdfPath = result.pdfFileUri.replaceFirst("file://", "");
return File(pdfPath).readAsBytes();
return (pdf, amount);
}
class ScanScreen extends HookConsumerWidget {
@@ -52,8 +38,8 @@ class ScanScreen extends HookConsumerWidget {
restartScan() async {
try {
final val = ref.refresh(_scanDocumentProvider);
Logger.root.info("Load again startup result: $val");
ref.invalidate(_scanDocumentProvider);
Logger.root.info("Load again startup");
} catch (e, s) {
Logger.root.shout("Failed to try again startup loading! $e $s");
}
@@ -62,21 +48,24 @@ class ScanScreen extends HookConsumerWidget {
return Padding(
padding: const EdgeInsets.all(8.0),
child: switch (scanDocProvider) {
AsyncData(:final value) when value != null => ExpenseEditor(
file: value,
AsyncData(:final value) when value.$1 != null => ExpenseEditor(
file: value.$1!,
initialData: value.$2,
onFinished: (expense) async {
await expenses.add(
info: expense,
fileContent: value,
fileContent: value.$1!,
fileMimeType: "application/pdf",
);
restartScan();
if (context.mounted) {
context.pushReplacement(scansPage);
}
},
onRescan: restartScan,
),
// No data
AsyncData(:final value) when value == null => ScanErrorScreen(
AsyncData(:final value) when value.$1 == null => ScanErrorScreen(
message: "No document scanned!",
onTryAgain: restartScan,
),

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

@@ -1,7 +1,12 @@
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';
@@ -18,14 +23,21 @@ class ScansListScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final expensesList = ref.watch(_expensesListProvider);
return Scaffold(
appBar: AppBar(title: Text("Expenses")),
body: switch (expensesList) {
AsyncData(:final value) => _ExpensesList(list: value),
AsyncError(:final error) => Center(child: Text('Load error: $error')),
_ => const Center(child: CircularProgressIndicator()),
},
);
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"),
};
}
}
@@ -36,10 +48,17 @@ class _ExpensesList extends StatelessWidget {
@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, id) {
final expense = list[id];
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",

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:moneymgr_mobile/providers/settings.dart';
import 'package:moneymgr_mobile/services/storage/prefs.dart';
import 'package:moneymgr_mobile/utils/extensions.dart';
class SettingsScreen extends ConsumerWidget {
@@ -8,6 +9,7 @@ class SettingsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(prefsProvider).requireValue;
final themeMode = ref.watch(currentThemeModeProvider);
void onTapThemeMode() =>
@@ -15,6 +17,16 @@ class SettingsScreen extends ConsumerWidget {
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(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
@@ -25,6 +37,22 @@ class SettingsScreen extends ConsumerWidget {
trailing: Text(themeMode.label),
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(),
ListTile(
leading: const Icon(Icons.info_outline),

View File

@@ -24,7 +24,11 @@ class ApiClient {
: client = Dio(BaseOptions(baseUrl: token.apiUrl));
/// 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");
return client.request(
uri,
@@ -32,6 +36,7 @@ class ApiClient {
method: method,
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,9 +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/profile/profile_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/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/scaffold_with_navigation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -28,6 +30,8 @@ GoRouter router(Ref ref) {
authStateNotifier.value = value;
});
final prefs = ref.read(prefsProvider).requireValue;
// 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
// of the scaffold.
@@ -36,7 +40,7 @@ GoRouter router(Ref ref) {
// see [AuthState] enum.
final navigationItems = [
NavigationItem(
path: scanPage,
path: capturePage,
body: (_) => ScanScreen(),
icon: Icons.camera_alt_outlined,
selectedIcon: Icons.camera_alt,
@@ -48,6 +52,15 @@ GoRouter router(Ref ref) {
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(
path: profilePage,
@@ -60,22 +73,22 @@ GoRouter router(Ref ref) {
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: navigationItems.first.path,
initialLocation: prefs.startOnScansListScreen() ? scansPage : capturePage,
routes: [
GoRoute(path: homePage, builder: (_, __) => const Scaffold()),
GoRoute(path: authPage, builder: (_, __) => const LoginScreen()),
GoRoute(path: qrAuthPath, builder: (_, __) => const QrAuthScreen()),
GoRoute(path: homePage, builder: (_, _) => const Scaffold()),
GoRoute(path: authPage, builder: (_, _) => const LoginScreen()),
GoRoute(path: qrAuthPath, builder: (_, _) => const QrAuthScreen()),
GoRoute(
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
// should be defined in [navigationItems]. Modification to this [ShellRoute]
// config is rarely needed.
ShellRoute(
builder: (_, __, child) => child,
builder: (_, _, child) => child,
routes: [
for (final (index, item) in navigationItems.indexed)
GoRoute(

View File

@@ -14,7 +14,7 @@ const manualAuthPage = "/login/manual";
const settingsPage = "/settings";
/// Scan path
const scanPage = "/scan";
const capturePage = "/scan";
/// Scans page
const scansPage = "/scans";

View File

@@ -1,6 +1,7 @@
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';
@@ -8,17 +9,23 @@ 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';
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
@@ -45,6 +52,10 @@ abstract class Expense with _$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";
@@ -109,7 +120,7 @@ class ExpensesManager {
id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000),
label: info.label,
cost: info.cost,
time: (info.time.millisecondsSinceEpoch / 1000).floor(),
time: info.timeAsSeconds,
mimeType: fileMimeType,
);
@@ -128,4 +139,45 @@ class ExpensesManager {
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 {
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() {
final json = getString("serverConfig");
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

@@ -6,14 +6,40 @@ 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 {
Future<Uint8List> renderPdf({String? path, Uint8List? pdfBytes}) async {
assert(path != null || pdfBytes != null);
// Create temporary file if required

View File

@@ -16,24 +16,44 @@ 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,
required this.onRescan,
this.onRescan,
this.onDelete,
this.initialData,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverConfig = ref.watch(prefsProvider).requireValue.serverConfig()!;
final labelController = useTextEditingController();
final costController = useTextEditingController();
final timeController = useState(DateTime.now());
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(
@@ -59,6 +79,11 @@ class ExpenseEditor extends HookConsumerWidget {
time: timeController.value,
),
);
// Reset screen after a scan
await pending.value;
labelController.text = "";
costController.text = "";
}
// Cancel operation
@@ -72,15 +97,46 @@ class ExpenseEditor extends HookConsumerWidget {
}
}
// 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
IconButton(
onPressed: onRescan == null ? null : handleRescan,
icon: Icon(Icons.restart_alt),
),
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
@@ -96,7 +152,10 @@ class ExpenseEditor extends HookConsumerWidget {
children: [
// Expense preview
Expanded(
child: PDFViewer(pdfBytes: file, fit: BoxFit.contain),
child: GestureDetector(
onTap: handleFullScreenInvoice,
child: PDFViewer(pdfBytes: file, fit: BoxFit.contain),
),
),
SizedBox(height: 10),
@@ -108,7 +167,13 @@ class ExpenseEditor extends HookConsumerWidget {
decimal: true,
signed: false,
),
decoration: const InputDecoration(labelText: 'Cost'),
decoration: InputDecoration(
labelText: 'Cost',
suffixIcon: IconButton(
onPressed: handleClearCost,
icon: const Icon(Icons.clear),
),
),
textInputAction: TextInputAction.done,
),

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,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
description:
name: built_value
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27"
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
url: "https://pub.dev"
source: hosted
version: "8.10.1"
version: "8.11.0"
characters:
dependency: transitive
description:
@@ -370,10 +370,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "6.0.0"
flutter_native_splash:
dependency: "direct main"
description:
@@ -452,18 +452,18 @@ packages:
dependency: "direct dev"
description:
name: freezed
sha256: "6022db4c7bfa626841b2a10f34dd1e1b68e8f8f9650db6112dcdeeca45ca793c"
sha256: "2d399f823b8849663744d2a9ddcce01c49268fb4170d0442a655bf6a2f47be22"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
version: "3.1.0"
freezed_annotation:
dependency: "direct main"
description:
name: freezed_annotation
sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.1.0"
frontend_server_client:
dependency: transitive
description:
@@ -484,10 +484,26 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: ac294be30ba841830cfa146e5a3b22bb09f8dc5a0fdd9ca9332b04b0bde99ebf
sha256: c489908a54ce2131f1d1b7cc631af9c1a06fac5ca7c449e959192089f9489431
url: "https://pub.dev"
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:
dependency: transitive
description:
@@ -529,7 +545,7 @@ packages:
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
dependency: "direct main"
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
@@ -604,10 +620,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "6.0.0"
logging:
dependency: "direct main"
description:
@@ -921,10 +937,10 @@ packages:
dependency: transitive
description:
name: source_helper
sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c"
sha256: "4f81479fe5194a622cdd1713fe1ecb683a6e6c85cd8cec8e2e35ee5ab3fdf2a1"
url: "https://pub.dev"
source: hosted
version: "1.3.5"
version: "1.3.6"
source_span:
dependency: transitive
description:

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -11,39 +11,39 @@
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/roboto": "^5.2.6",
"@emotion/styled": "^11.14.1",
"@fontsource/roboto": "^5.2.8",
"@jsonjoy.com/base64": "^1.1.2",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^7.1.2",
"@mui/material": "^7.1.2",
"@mui/x-charts": "^8.5.3",
"@mui/x-data-grid": "^8.5.3",
"@mui/x-date-pickers": "^8.5.3",
"@mui/x-charts": "^8.10.2",
"@mui/x-data-grid": "^8.9.2",
"@mui/x-date-pickers": "^8.9.2",
"date-and-time": "^3.6.0",
"dayjs": "^1.11.13",
"dayjs": "^1.11.18",
"filesize": "^10.1.6",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.2",
"react-router-dom": "^7.6.2",
"ts-pattern": "^5.7.1"
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router": "^7.6.3",
"react-router-dom": "^7.6.3",
"ts-pattern": "^5.8.0"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"eslint": "^9.26.0",
"eslint-plugin-react-dom": "^1.49.0",
"@eslint/js": "^9.33.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.32.0",
"eslint-plugin-react-dom": "^1.53.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^00.4.20",
"eslint-plugin-react-x": "^1.52.2",
"globals": "^16.2.0",
"eslint-plugin-react-x": "^1.52.9",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"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 {
auth_disabled: boolean;
oidc_provider_name: string;
apk_download_url: string;
accounts_types: AccountType[];
constraints: ServerConstraints;
}

View File

@@ -2,9 +2,11 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import DriveFileMoveOutlineIcon from "@mui/icons-material/DriveFileMoveOutline";
import LinkOffIcon from "@mui/icons-material/LinkOff";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import ClearIcon from "@mui/icons-material/Clear";
import RefreshIcon from "@mui/icons-material/Refresh";
import {
IconButton,
InputAdornment,
ListItemIcon,
ListItemText,
TextField,
@@ -392,6 +394,19 @@ function MovementsTable(p: {
setFilter(e.target.value);
}}
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>
<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
information as they won't be available next.
<br />
API URL : <CopyTextChip text={APIClient.ActualBackendURL()} />
<br />
Token ID: <CopyTextChip text={p.token.id.toString()} />
<br />
Token value: <CopyTextChip text={p.token.token} />

View File

@@ -1,5 +1,6 @@
import { mdiApi, mdiCash } from "@mdi/js";
import Icon from "@mdi/react";
import AndroidIcon from "@mui/icons-material/Android";
import CloudDownloadIcon from "@mui/icons-material/CloudDownload";
import LogoutIcon from "@mui/icons-material/Logout";
import SettingsIcon from "@mui/icons-material/Settings";
@@ -10,6 +11,7 @@ import MenuItem from "@mui/material/MenuItem";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import * as React from "react";
import { ServerApi } from "../api/ServerApi";
import { useAuthInfo } from "./BaseAuthenticatedPage";
import { DarkThemeButton } from "./DarkThemeButtonWidget";
import { PublicModeButton } from "./PublicModeButtonWidget";
@@ -100,6 +102,18 @@ export function MoneyWebAppBar(p: {
</MenuItem>
</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 />
{/* Sign out */}