Compare commits

...

240 commits

Author SHA1 Message Date
ae2a6b8b32 feature/prepare-1.6.4-22-release (#135)
All checks were successful
/ build (push) Successful in 4m46s
Reviewed-on: #135
2024-11-01 14:36:36 +00:00
dce3c916a2 chore(release): prepare release of 1.6.4.+22
All checks were successful
/ build (push) Successful in 4m19s
2024-11-01 15:31:20 +01:00
58141fe646 chore(deps,build): move to latest gradle wrapper and AGP, apply all possible updates
All checks were successful
/ build (push) Successful in 4m23s
2024-11-01 15:21:38 +01:00
dd5abc48f9 fix(deps): update all patch dependencies (#127)
All checks were successful
/ build (push) Successful in 4m27s
file_picker (source) 	dependencies 	patch 	8.0.3 -> 8.0.5
stacked 	dependencies 	patch 	3.4.2 -> 3.4.3
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-07-01 07:16:42 +00:00
bf1f1e3295 chore(deps,ci): Updates and group renovate updates together
All checks were successful
/ build (push) Successful in 5m55s
2024-06-08 10:35:06 +02:00
e7463ffaef Upgrades #noissue
All checks were successful
/ build (push) Successful in 5m10s
2024-05-01 09:45:22 +02:00
ae454abb3a Update dependency url_launcher to v6.2.6 (#115)
All checks were successful
/ build (push) Successful in 5m19s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-04-28 14:29:09 +00:00
34bcd2ee92 Update ghcr.io/cirruslabs/flutter Docker tag to v3.19.6 (#116)
All checks were successful
/ build (push) Successful in 5m24s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-04-26 19:46:10 +00:00
d2485a91cd Upgrades #noissue
All checks were successful
/ build (push) Successful in 5m6s
2024-04-02 09:04:43 +02:00
e8c8808c0e Change Renovate schedule to be monthly #noissue
All checks were successful
/ build (push) Successful in 5m19s
2024-03-06 23:40:45 +01:00
b544003941 Update dependency provider to v6.1.2 (#98)
All checks were successful
/ build (push) Successful in 5m20s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-03-02 09:03:30 +00:00
5668c15547 Update ghcr.io/cirruslabs/flutter Docker tag to v3.19.2 (#99)
All checks were successful
/ build (push) Successful in 5m25s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-03-02 08:19:56 +00:00
7f145f6960 Some upgrades #noissue
All checks were successful
/ build (push) Successful in 5m23s
2024-02-26 22:56:03 +01:00
d0dd798008 Update dependency device_info_plus to v9.1.2 (#93)
All checks were successful
/ build (push) Successful in 5m55s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-02-05 23:07:48 +00:00
f16ee08079 Update dependency share_plus to v7.2.2 (#94)
All checks were successful
/ build (push) Successful in 5m40s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-02-05 22:14:38 +00:00
a4a3c2e5dc Build per-abi in fdroid fastlane script #noissue
All checks were successful
/ build (push) Successful in 5m33s
2024-02-05 22:52:45 +01:00
a5e9597207 Update dependency stacked to v3.4.2 (#92)
All checks were successful
/ build (push) Successful in 5m34s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-02-01 22:03:47 +00:00
f460c68339 Fix renovate schedule #noissue
All checks were successful
/ build (push) Successful in 5m33s
2024-01-26 13:46:49 +01:00
c7af6a874d Bump Flutter in pipeline; adapt renovate to provide separated pull requests for all dependencies and ignore intl and path as they are bound to underlying flutter version (or the test dependency) #noissue
All checks were successful
/ build (push) Successful in 5m31s
2024-01-26 09:28:02 +01:00
e0973735d8 Update dependencies
All checks were successful
/ build (push) Successful in 6m3s
2024-01-25 20:40:48 +01:00
dc715e38d1 Update all patch dependencies (#87)
All checks were successful
/ build (push) Successful in 6m5s
get_it 	dependencies 	patch 	7.6.4 -> 7.6.6
ghcr.io/cirruslabs/flutter 	container 	patch 	3.16.4 -> 3.16.6
url_launcher (source) 	dependencies 	patch 	6.2.2 -> 6.2.3
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2024-01-10 22:16:53 +00:00
eb1d949af8 release/prepare-next-dev-cycle (#86)
All checks were successful
/ build (push) Successful in 5m17s
Reviewed-on: #86
2023-12-16 18:47:43 +00:00
4a715aba48 Prepare next dev cycle
All checks were successful
/ build (push) Successful in 5m42s
2023-12-16 19:37:55 +01:00
c48c16d63d release/1.6.3 (#85)
All checks were successful
/ build (push) Successful in 5m30s
Reviewed-on: #85
2023-12-16 18:29:59 +00:00
57db10e912 Prepare release of 1.6.3
All checks were successful
/ build (push) Successful in 5m43s
2023-12-16 19:23:38 +01:00
a4defff982 Fixed not receiving sharing intents, reduced renovate frequency (#84)
All checks were successful
/ build (push) Successful in 5m36s
2023-12-16 18:59:00 +01:00
3ae2c37813 Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.4 (#83)
All checks were successful
/ build (push) Successful in 5m52s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-12-15 00:50:41 +00:00
d9b33feac9 Update dependency built_value_generator to v8.8.1 (#82)
All checks were successful
/ build (push) Successful in 5m30s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-12-12 21:55:16 +00:00
941b7c81ad Update dependency url_launcher to v6.2.2 (#81)
All checks were successful
/ build (push) Successful in 5m34s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-12-09 09:10:32 +00:00
5bc02638f7 Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.3 (#80)
All checks were successful
/ build (push) Successful in 5m35s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-12-08 14:55:39 +00:00
477e557d31 Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.2 (#78)
All checks were successful
/ build (push) Successful in 5m23s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-12-06 23:43:44 +00:00
c917e478a6 release/prepare-next-dev-cycle (#77)
All checks were successful
/ build (push) Successful in 5m42s
Reviewed-on: #77
2023-12-01 18:35:41 +00:00
8b96610fa6 Prepare next dev cycle #noissue
All checks were successful
/ build (push) Successful in 5m29s
2023-12-01 19:20:13 +01:00
df40808bd7 release/1.6.2+20 (#76)
All checks were successful
/ build (push) Successful in 5m57s
Reviewed-on: #76
2023-12-01 18:15:51 +00:00
dfc5772b93 Prepare release of 1.6.2+20 #noissue
All checks were successful
/ build (push) Successful in 5m32s
2023-12-01 19:09:33 +01:00
3d6ae1a9ef Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.1 (#75)
All checks were successful
/ build (push) Successful in 5m14s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-11-29 21:08:49 +00:00
e49dd86347 Update dependency build_runner to v2.4.7 (#74)
All checks were successful
/ build (push) Successful in 5m24s
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-11-28 21:46:18 +00:00
4cd6165d9b Update dependency http to v1.1.2 (#71)
All checks were successful
/ build (push) Successful in 5m24s
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [http](https://github.com/dart-lang/http) | dependencies | patch | `1.1.0` -> `1.1.2` |
Co-authored-by: Renovate Bot <renovate@myservermanager.com>
Co-committed-by: Renovate Bot <renovate@myservermanager.com>
2023-11-27 18:48:53 +00:00
e661171fd2 Various improvements and #noissue
All checks were successful
/ build (push) Successful in 5m17s
- Bumped Android minSdk to 30 (Android 11)
- Fixed permission service not handling Android SDK 33 correctly
- Fixed permission service not being started during application start
2023-11-26 23:53:17 +01:00
c99df89cb0 Update transitive dependencies #noissue 2023-11-26 20:48:47 +01:00
03d61dcdd0 Merge pull request 'Update dependency package_info_plus to v5' (#73) from renovate/package_info_plus-5.x into develop
All checks were successful
/ build (push) Successful in 5m42s
Reviewed-on: #73
2023-11-26 19:34:57 +00:00
f2d1083620 Update dependency package_info_plus to v5
All checks were successful
/ build (push) Successful in 5m17s
2023-11-25 00:01:06 +00:00
b5694ae9c6 Merge pull request 'Update all minor dependencies' (#72) from renovate/all-minor-deps into develop
All checks were successful
/ build (push) Successful in 5m37s
Reviewed-on: #72
2023-11-24 23:32:04 +00:00
9dd84b20a9 Update all minor dependencies
All checks were successful
/ build (push) Successful in 5m31s
2023-11-24 23:01:08 +00:00
8bb310fb40 Update lock file #noissue
All checks were successful
/ build (push) Successful in 5m17s
2023-11-18 14:10:44 +01:00
47987b8dad Merge pull request 'Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.0' (#70) from renovate/all-minor-deps into develop
All checks were successful
/ build (push) Successful in 5m18s
Reviewed-on: #70
2023-11-18 12:54:36 +00:00
3d68dcefab Update ghcr.io/cirruslabs/flutter Docker tag to v3.16.0
All checks were successful
/ build (push) Successful in 6m8s
2023-11-17 23:00:15 +00:00
b51131de8f Merge pull request 'Update dependency flutter_lints to v3' (#65) from renovate/flutter_lints-3.x into develop
All checks were successful
/ build (push) Successful in 5m0s
Reviewed-on: #65
2023-11-14 19:27:35 +00:00
7e871bb4b1 Fix flutter lints for next major version #noissue
All checks were successful
/ build (push) Successful in 5m5s
2023-11-14 20:22:12 +01:00
88b9d81bc2 Update dependency flutter_lints to v3
Some checks failed
/ build (push) Failing after 1m39s
2023-11-14 00:01:15 +00:00
7c7befe071 Merge pull request 'Update dependency file_picker to v6' (#61) from renovate/file_picker-6.x into develop
All checks were successful
/ build (push) Successful in 5m5s
Reviewed-on: #61
2023-11-13 23:30:24 +00:00
2461a02f9d Update dependency file_picker to v6
All checks were successful
/ build (push) Successful in 5m4s
2023-11-13 22:01:15 +00:00
15104d7e40 Renovate: bundle minor versions
All checks were successful
/ build (push) Successful in 5m7s
2023-11-13 21:54:08 +00:00
733f09baab Merge pull request 'Update dependency share_plus to v7.2.1' (#63) from renovate/share_plus-7.x into develop
All checks were successful
/ build (push) Successful in 4m56s
Reviewed-on: #63
2023-11-13 21:42:27 +00:00
1758eeed18 Update dependency share_plus to v7.2.1
All checks were successful
/ build (push) Successful in 4m58s
2023-11-12 17:01:17 +00:00
c9764e780b Merge pull request 'Update dependency provider to v6.1.1' (#69) from renovate/provider-6.x into develop
All checks were successful
/ build (push) Successful in 5m11s
Reviewed-on: #69
2023-11-12 16:06:07 +00:00
3754d71dec Update dependency provider to v6.1.1
All checks were successful
/ build (push) Successful in 5m0s
2023-11-10 23:01:11 +00:00
8904d49eb2 Merge pull request 'Update dependency package_info_plus to v4.2.0' (#62) from renovate/package_info_plus-4.x into develop
All checks were successful
/ build (push) Successful in 4m54s
Reviewed-on: #62
2023-11-07 00:18:34 +00:00
0e7bc55bef Update dependency package_info_plus to v4.2.0
All checks were successful
/ build (push) Successful in 4m58s
2023-10-31 08:01:12 +00:00
089d07846d Merge pull request 'Update dependency url_launcher to v6.2.1' (#67) from renovate/url_launcher-6.x into develop
All checks were successful
/ build (push) Successful in 4m38s
Reviewed-on: #67
2023-10-31 07:38:36 +00:00
3f64f86acf Update dependency url_launcher to v6.2.1
All checks were successful
/ build (push) Successful in 4m49s
2023-10-31 00:01:21 +00:00
7c91d8b6ca Merge pull request 'Update dependency built_value_generator to v8.7.0' (#68) from renovate/built_value_generator-8.x into develop
All checks were successful
/ build (push) Successful in 4m39s
Reviewed-on: #68
2023-10-30 23:07:49 +00:00
df1b038782 Update dependency built_value_generator to v8.7.0
All checks were successful
/ build (push) Successful in 4m43s
2023-10-30 23:01:11 +00:00
c9ef125abb Merge pull request 'Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.9' (#66) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 4m58s
Reviewed-on: #66
2023-10-30 22:42:52 +00:00
5a8750b877 Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.9
All checks were successful
/ build (push) Successful in 5m15s
2023-10-26 20:00:17 +00:00
8c10e42c0f Merge pull request 'Update all patch dependencies' (#64) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 4m47s
Reviewed-on: #64
2023-10-22 11:50:21 +00:00
96d8cbea99 Update all patch dependencies
All checks were successful
/ build (push) Successful in 5m16s
2023-10-20 22:01:12 +00:00
685b140135 Merge pull request 'Update dependency shared_preferences to v2.2.2' (#60) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 4m43s
Reviewed-on: #60
2023-10-12 07:48:57 +00:00
f51e781a9f Update dependency shared_preferences to v2.2.2
All checks were successful
/ build (push) Successful in 4m45s
2023-10-12 00:01:22 +00:00
16c1a597fc Merge pull request 'Update dependency permission_handler to v11.0.1' (#59) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 4m39s
Reviewed-on: #59
2023-10-08 11:14:45 +00:00
71ac72a6cc Update dependency permission_handler to v11.0.1
All checks were successful
/ build (push) Successful in 4m40s
2023-10-06 22:01:13 +00:00
0bc3ae1da8 Merge pull request 'Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.6' (#58) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m4s
Reviewed-on: #58
2023-10-02 07:02:40 +00:00
fc5b8783d3 Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.6
All checks were successful
/ build (push) Successful in 6m35s
2023-09-29 22:00:18 +00:00
5492fed0b5 Updated internal dependencies and moved progress indicator of Show Configuration into button #noissue
All checks were successful
/ build (push) Successful in 5m55s
2023-09-24 17:43:37 +02:00
30810868b2 Merge pull request 'Update all patch dependencies' (#57) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m24s
Reviewed-on: #57
2023-09-23 16:44:40 +00:00
a889a6fbee Update all patch dependencies
All checks were successful
/ build (push) Successful in 6m32s
2023-09-22 22:01:09 +00:00
36e5bcab1f Merge pull request 'Update dependency dynamic_color to v1.6.7' (#56) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m29s
Reviewed-on: #56
2023-09-16 07:09:22 +00:00
3b06c2b80b Update dependency dynamic_color to v1.6.7
All checks were successful
/ build (push) Successful in 5m19s
2023-09-15 10:01:11 +00:00
e4cfa81672 Merge pull request 'Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.4' (#55) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m18s
Reviewed-on: #55
2023-09-14 16:22:19 +00:00
2837054383 Update ghcr.io/cirruslabs/flutter Docker tag to v3.13.4
All checks were successful
/ build (push) Successful in 5m51s
2023-09-14 10:00:21 +00:00
4deb9b92cb Merge pull request 'Update dependency permission_handler to v11' (#54) from renovate/permission_handler-11.x into develop
All checks were successful
/ build (push) Successful in 6m13s
Reviewed-on: #54
2023-09-11 17:13:37 +00:00
b0edda0432 Update dependency permission_handler to v11
All checks were successful
/ build (push) Successful in 6m9s
2023-09-11 09:01:11 +00:00
886632957d Merge pull request 'Update dependency permission_handler to v10.4.5' (#53) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m3s
Reviewed-on: #53
2023-09-06 22:08:45 +00:00
264609dff3 Update dependency permission_handler to v10.4.5
All checks were successful
/ build (push) Successful in 6m5s
2023-09-06 14:01:23 +00:00
d87796f47b Merge pull request 'Update dependency get_it to v7.6.4' (#52) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m10s
Reviewed-on: #52
2023-09-04 14:57:28 +00:00
42f40d52d2 Update dependency get_it to v7.6.4
All checks were successful
/ build (push) Successful in 6m5s
2023-09-04 14:01:23 +00:00
292660a970 Merge pull request 'Update dependency logger to v2.0.2' (#51) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m8s
Reviewed-on: #51
2023-09-04 00:08:04 +00:00
51e2c476c7 Update dependency logger to v2.0.2
All checks were successful
/ build (push) Successful in 6m1s
2023-09-03 22:01:17 +00:00
44cc8bac28 Merge pull request 'Update dependency flutter_sharing_intent to v1.1.0' (#50) from renovate/flutter_sharing_intent-1.x into develop
All checks were successful
/ build (push) Successful in 6m5s
Reviewed-on: #50
2023-09-03 08:18:45 +00:00
9b571fb1a4 Update dependency flutter_sharing_intent to v1.1.0
All checks were successful
/ build (push) Successful in 6m12s
2023-09-03 08:01:14 +00:00
d63ef288eb Merge pull request 'Update all patch dependencies' (#49) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m5s
Reviewed-on: #49
2023-08-31 21:47:36 +00:00
549b218922 Update all patch dependencies
All checks were successful
/ build (push) Successful in 6m5s
2023-08-31 20:01:12 +00:00
f61c5a4a1b Merge pull request 'Update dependency file_picker to v5.5.0' (#48) from renovate/file_picker-5.x into develop
All checks were successful
/ build (push) Successful in 6m28s
Reviewed-on: #48
2023-08-30 22:33:32 +00:00
a06bbf6751 Update dependency file_picker to v5.5.0
All checks were successful
/ build (push) Successful in 6m9s
2023-08-30 20:14:30 +00:00
e980f1ae53 Merge pull request 'Update all patch dependencies' (#47) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m1s
Reviewed-on: #47
2023-08-30 20:14:19 +00:00
66a5f4e9af Update all patch dependencies
All checks were successful
/ build (push) Successful in 5m56s
2023-08-30 16:01:16 +00:00
39b1465efc Merge pull request 'Update dependency flutter_lints to v2.0.3' (#46) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 6m2s
Reviewed-on: #46
2023-08-29 18:40:21 +00:00
22d096ae43 Update dependency flutter_lints to v2.0.3
All checks were successful
/ build (push) Successful in 6m6s
2023-08-29 18:01:20 +00:00
08d935284d Merge pull request 'Update dependency file_picker to v5.3.4' (#45) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m57s
Reviewed-on: #45
2023-08-29 06:39:29 +00:00
6b87873cc7 Update dependency file_picker to v5.3.4
All checks were successful
/ build (push) Successful in 6m4s
2023-08-26 00:01:18 +00:00
41987331b0 Merge pull request 'Update dependency built_value_generator to v8.6.2' (#44) from renovate/all-patch-deps into develop
All checks were successful
/ build (push) Successful in 5m58s
Reviewed-on: #44
2023-08-24 11:04:38 +00:00
f19d23a757 Update dependency built_value_generator to v8.6.2
All checks were successful
/ build (push) Successful in 6m45s
2023-08-24 09:01:25 +00:00
67076779a3 Upgrade Flutter to 3.13.0 and Dart to 3.1 #noissue (#43)
All checks were successful
/ build (push) Successful in 5m54s
Reviewed-on: #43
Co-authored-by: Varakh <varakh@varakh.de>
Co-committed-by: Varakh <varakh@varakh.de>
2023-08-17 21:56:23 +00:00
6d302c4392 Merge pull request 'Move to Forgejo as pipeline #noissue' (#41) from feature/move-to-forgejo into develop
All checks were successful
/ build (push) Successful in 5m56s
Reviewed-on: #41
2023-08-15 19:54:47 +00:00
53cd587a42 Update renovate.json
All checks were successful
/ build (push) Successful in 5m56s
2023-08-15 19:28:25 +00:00
769dd7e513 Move to Forgejo as pipeline and configure Renovate to update all patch dependencies in one PR #noissue
All checks were successful
/ build (push) Successful in 5m52s
2023-08-15 19:05:29 +00:00
a3ee146e25 Merge pull request 'Update dependency package_info_plus to v4.1.0' (#39) from renovate/package_info_plus-4.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #39
2023-08-10 18:53:51 +00:00
5e9947969e Update dependency package_info_plus to v4.1.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-02 13:02:12 +00:00
689c8b4408 Update renovate config to align with recommendations #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-28 21:36:47 +00:00
7cee219b19 Merge pull request 'Update ghcr.io/cirruslabs/flutter Docker tag to v3.12.0' (#34) from renovate/ghcr.io-cirruslabs-flutter-3.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2023-07-27 19:24:16 +00:00
803cd11cc4 Upgrade docker image for pipeline #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-27 21:12:56 +02:00
a99662af3c Update ghcr.io/cirruslabs/flutter Docker tag to v3.12.0
Some checks failed
continuous-integration/drone/push Build is failing
2023-07-27 18:17:48 +00:00
3d11fde4e9 Add back ignoring intl as it depends on underlying flutter engine #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-27 18:17:18 +00:00
43cb6472f2 Remove ignoring intl for renovate #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-27 07:15:53 +00:00
67dafa615f Merge pull request 'Update dependency logger to v2' (#36) from renovate/logger-2.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #36
2023-07-26 20:51:54 +00:00
fc0fe31b70 Fix signature change in upgraded logger #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-26 22:42:51 +02:00
0e4dcbde22 Update dependency logger to v2
Some checks failed
continuous-integration/drone/push Build is failing
2023-07-26 19:01:45 +00:00
65a0a294a6 Merge pull request 'Update dependency http to v1' (#35) from renovate/http-1.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #35
2023-07-26 18:28:53 +00:00
548d02362b Update dependency http to v1
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-26 07:01:45 +00:00
c50e9ec7ba Merge pull request 'Update dependency shared_preferences to v2.2.0' (#32) from renovate/shared_preferences-2.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #32
2023-07-26 06:24:09 +00:00
e5518b4ade Merge pull request 'Update dependency share_plus to v7.0.2' (#28) from renovate/share_plus-7.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2023-07-26 06:24:01 +00:00
0b093302ec Merge pull request 'Update dependency json_serializable to v6.7.1' (#26) from renovate/json_serializable-6.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #26
2023-07-26 06:23:53 +00:00
4114feb56e Update dependency shared_preferences to v2.2.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 23:01:56 +00:00
d8c9027509 Update dependency share_plus to v7.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 23:01:52 +00:00
e1075f5725 Update dependency json_serializable to v6.7.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 23:01:47 +00:00
b49f10c875 Merge pull request 'Update dependency stacked to v3.4.1' (#33) from renovate/stacked-3.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2023-07-25 22:01:45 +00:00
494420cbd3 Merge pull request 'Update dependency logger to v1.4.0' (#30) from renovate/logger-1.x into develop
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #30
2023-07-25 22:01:36 +00:00
aedc7f2bbb Merge pull request 'Update dependency flutter_lints to v2.0.2' (#25) from renovate/flutter_lints-2.x into develop
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #25
2023-07-25 22:01:21 +00:00
9ac363070b Merge pull request 'Update dependency package_info_plus to v4.0.2' (#27) from renovate/package_info_plus-4.x into develop
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #27
2023-07-25 22:01:10 +00:00
580099856f Merge pull request 'Update dependency url_launcher to v6.1.12' (#29) from renovate/url_launcher-6.x into develop
Some checks reported errors
continuous-integration/drone/push Build was killed
Reviewed-on: #29
2023-07-25 22:00:34 +00:00
d56d717de0 Merge pull request 'Update dependency file_picker to v5.3.3' (#24) from renovate/file_picker-5.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #24
2023-07-25 21:59:59 +00:00
3cf96ad5ab Update dependency stacked to v3.4.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:38 +00:00
26d0e0851b Update dependency logger to v1.4.0
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:29 +00:00
c772d950e2 Update dependency url_launcher to v6.1.12
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:25 +00:00
0630035e45 Update dependency package_info_plus to v4.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:17 +00:00
3fb46fc766 Update dependency flutter_lints to v2.0.2
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:08 +00:00
53c65ce54f Update dependency file_picker to v5.3.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 17:02:02 +00:00
9a1db51f23 Merge pull request 'Update dependency permission_handler to v10.4.3' (#31) from renovate/permission_handler-10.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #31
2023-07-25 16:19:36 +00:00
664b734544 Merge pull request 'Update dependency dynamic_color to v1.6.6' (#23) from renovate/dynamic_color-1.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2023-07-25 06:02:38 +00:00
0e69e904b4 Update dependency permission_handler to v10.4.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 05:53:58 +00:00
09b608a168 Update dependency dynamic_color to v1.6.6
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 05:53:17 +00:00
091e344831 Merge pull request 'Update dependency built_value_generator to v8.6.1' (#21) from renovate/built_value_generator-8.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
2023-07-25 05:50:47 +00:00
0f9e8b398f Update dependency built_value_generator to v8.6.1
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 05:40:26 +00:00
88d85257a7 Merge pull request 'Update dependency build_runner to v2.4.6' (#20) from renovate/build_runner-2.x into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2023-07-25 05:36:06 +00:00
06566d3d6e Update dependency build_runner to v2.4.6
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 05:25:33 +00:00
35bdd06f72 Merge pull request 'Configure Renovate' (#19) from renovate/configure into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #19
2023-07-25 05:23:40 +00:00
e59164e803 Ignore android and ios folders
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 07:17:04 +02:00
eec38b455c Add renovate config
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 07:12:13 +02:00
ee6230399a Add renovate.json
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 05:04:32 +00:00
31d91bafc1 Remove renovate pipeline step #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-25 07:00:55 +02:00
82f9ab5535 Merge pull request 'Add fdroid build instructions and fastlane script #noissue' (#18) from feature/add-izzy-on-droid into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #18
2023-07-24 21:51:24 +00:00
66493cff91 Add fdroid build instructions and fastlane script #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-24 23:42:08 +02:00
210c3e7aa4 Add renovate pipeline step #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-24 23:39:21 +02:00
d6b645112e Merge pull request 'feature/prepare-next-dev' (#16) from feature/prepare-next-dev into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #16
2023-05-29 14:23:17 +00:00
713d8f57be Bump version to next develop #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 16:14:59 +02:00
bc777d4826 Add documentation for release with bundler and fastlane #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 15:59:59 +02:00
ad9a3a15f5 Prepare next develop cycle #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 15:47:12 +02:00
dbb4929939 Merge pull request 'Prepare release of 1.6.1+19' (#15) from develop into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #15
2023-05-29 13:43:26 +00:00
adb55fd73b Prepare release of latest develop
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 15:35:47 +02:00
004133f0c0 Restore to initial state #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-29 15:25:57 +02:00
67903d1331 Try out larger executor #noissue
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-05-29 15:10:34 +02:00
0a0fbe039b Try out larger executor #noissue
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2023-05-29 15:09:53 +02:00
6ef8a9daa0 Restore old state for pipeline #noissue
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-29 15:04:37 +02:00
47b45a9084 Enforce runner #noissue
Some checks are pending
continuous-integration/drone/push Build is pending
2023-05-29 15:03:30 +02:00
06dab63e05 Enforce runner #noissue
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-05-29 14:53:38 +02:00
9c331cf6d3 Enforce runner #noissue
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-05-29 14:35:45 +02:00
40451dbabc Determine runner size #noissue
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-05-29 14:26:27 +02:00
6bb4c177e2 Merge pull request 'Upgrade dependencies and minSdk version #noissue' (#14) from feature/upgrade-deps into develop
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #14
2023-05-28 20:37:08 +00:00
81eb1af2bd Upgrade dependencies and minSdk version #noissue
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-05-28 18:20:18 +02:00
3e7bc379f1 Only trigger main build pipeline for push and pull requests #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-20 18:58:25 +01:00
e9929bd3c3 Remove drone signing and rename pipeline stage to build #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-20 18:18:16 +01:00
9386661adb Enforce drone yml signing #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-03-20 09:03:18 +01:00
46a6df9f80 Merge pull request 'Upgrade dependencies #noissue' (#13) from feature/upgrade-dependencies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #13
2023-03-17 20:22:02 +00:00
577f9cd42c Upgrade dependencies #noissue
All checks were successful
continuous-integration/drone Build is passing
2023-03-17 21:10:41 +01:00
d8d89167a9 Merge pull request 'Prepare next dev cycle #noissue' (#12) from release/prepare-next-dev-cycle into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #12
2023-01-16 18:47:13 +00:00
6d75672add Prepare next dev cycle #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 19:39:18 +01:00
fc88d5c22f Merge pull request 'bugfix/10-library-prevents-compile-and-wrong-color-in-login' (#11) from bugfix/10-library-prevents-compile-and-wrong-color-in-login into master
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Reviewed-on: #11
2023-01-16 18:32:59 +00:00
a2ee915463 #10 Adapt changelog
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 19:22:35 +01:00
6e5fa0eaa8 #10 Increase semantic version
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 19:14:56 +01:00
8c3bf06b87 10: update dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-16 01:53:34 +01:00
5e9ac38ca0 10: Reformat code 2023-01-16 01:44:51 +01:00
a5dab51765 10: Fixed input colors in login view when using dark theme; Added removal of individual files selected for upload; Added size for individual files selected for upload; Replaced intent sharing library with flutter_sharing_intent 2023-01-16 01:43:37 +01:00
a65c7d9253 Merge pull request 'Added proper linting #noissue' (#9) from feature/implement-linting into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #9
2023-01-04 20:42:53 +00:00
bac39aebdf Update build runner #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-04 21:23:21 +01:00
b55e932204 Added proper linting #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2023-01-04 21:17:54 +01:00
f9a2bb0df7 Add license exception for app store distribution #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-27 12:28:49 +01:00
a0789e6883 Prepare next dev cycle 1.5.2+18 #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-16 22:57:10 +01:00
2e7778926a Release 1.5.1+17 #noissue
All checks were successful
continuous-integration/drone/tag Build is passing
2022-12-16 22:53:59 +01:00
c2c8ffc5c5 Prepare next dev cycle for 1.5.1+17 #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-16 17:20:09 +01:00
b18d63ce1e Prepare release of 1.5.0+16 #noissue
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-12-16 17:15:40 +01:00
06db805f82 Merge pull request 'Prepare release of 1.5.0+16 #7' (#8) from release/7-release-1.5.0+16 into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #8

Closes #7
2022-12-16 15:56:40 +00:00
3f927328f0 Prepare release of 1.5.0+16 #7
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-16 16:50:22 +01:00
8d11584811 Merge pull request 'feature/5-use-material-3-material-you #2 #5' (#6) from feature/5-use-material-3-material-you into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #6

Closes #2 #5
2022-12-16 15:46:52 +00:00
db9fef6798 Adapt min dart sdk version for pipeline #5
All checks were successful
continuous-integration/drone/push Build is passing
2022-12-16 16:33:01 +01:00
6fedb8c661 Update dependencies and flutter 3.3, use Material You and remove unsupported swipe for Material You navigation bar #5
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-16 16:30:07 +01:00
d3e54eb1d5 Merge pull request 'Update dependencies #noissue' (#5) from feature/update-dependencies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #5
2022-08-17 15:26:26 +00:00
874d734c80 Update dependencies #noissue
All checks were successful
continuous-integration/drone Build is passing
2022-08-17 17:18:51 +02:00
a6cdd407c0 Update links for main git repository #noissue
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-05 22:03:43 +02:00
583520ea7c Merge pull request 'Changed links for moving away from GitHub #noissue' (#3) from feature/move-away-from-github into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #3
2022-07-05 17:50:47 +00:00
77fee6ae32 Changed links for moving away from GitHub #noissue
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2022-07-05 18:58:27 +02:00
cd07b2fba2 Ensure pipeline uses same tested Flutter version
Some checks reported errors
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
continuous-integration/drone/tag Build was killed
2022-06-23 00:49:08 +02:00
011113d72b Reformat code with latest Flutter 3 linting 2022-06-23 00:44:08 +02:00
7ef179906d Increased target SDK to 33, Increased dart to 2.17.3, Indicate configuration loading in profile view 2022-06-23 00:42:37 +02:00
c8007e8415 Prepare next release cycle 1.4.2+16
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
2022-05-03 09:28:43 +02:00
a6851c5430 Release 1.4.2+15
All checks were successful
continuous-integration/drone/tag Build is passing
2022-05-03 09:19:52 +02:00
80818f5eda Update to latest build runner including all its dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2022-04-29 01:26:28 +02:00
3625461b57 Update to Android embedding v2, updated to Gradle 7, upgraded internal dependencies to latest versions 2022-04-29 01:11:08 +02:00
830deab2e1 Remove Travis integration
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-27 23:05:26 +02:00
15e1607072 Build debug on CI
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-22 10:13:54 +01:00
71dcab97be Add drone CI
Some checks failed
continuous-integration/drone Build is failing
2022-01-22 10:06:13 +01:00
3c06e3b29f Minor cleanup 2022-01-22 09:51:15 +01:00
2b1d8939d2 Prepare next release cycle for 1.4.2+15 2022-01-21 16:46:53 +01:00
927be613c9 Fixed opening links for 1.4.1+14 2022-01-21 16:45:39 +01:00
80f20f6032 Prepare next release cycle for 1.4.1+14 2022-01-20 17:41:07 +01:00
c82c5f8dc5 Replace jcenter with mavenCentral for build and publish 1.4.0+13 2022-01-20 17:40:13 +01:00
c16242d340 Replaced EOL share with share_plus plugin 2022-01-20 17:25:39 +01:00
207985c745 Upgraded to latest pub.dev versions 2022-01-20 01:21:10 +01:00
ccb780be50 Initial upgrade to null-safety and latest dependency versions 2022-01-20 01:08:20 +01:00
35e957a049 Use 4 spaces as tab 2021-05-21 01:33:16 +02:00
c337e8a7b8 Increased target SDK to 30 2021-05-19 12:08:29 +02:00
16636d9f7c Prepare next development cycle 2021-05-09 11:01:35 +02:00
aa07d61a3c Minor method refactor, release 1.3.3+12 2021-05-09 10:59:14 +02:00
9ec215cbf4 For now disable building APK with Travis CI 2021-05-08 15:00:02 +02:00
79593bd288 Use JDK Switcher to switch to OpenJDK8 in bionic image of Travis CI 2021-05-08 14:53:12 +02:00
707520b44c Use JDK Switcher to switch to OpenJDK8 in bionic image of Travis CI 2021-05-08 14:42:22 +02:00
57a502f0c3 Use JDK Switcher to switch to OpenJDK8 in bionic image of Travis CI 2021-05-08 14:34:31 +02:00
4fdcf54dc0 Switch back to bionic in Travis CI 2021-05-08 14:30:47 +02:00
c1255dcab5 Don't explicitly install JDK as it might be bundled already in Travis CI 2021-05-08 14:02:41 +02:00
b2101e2327 Move to focal ubuntu for Travis CI 2021-05-08 13:51:33 +02:00
9e740d31d4 Move to OpenJDK8 for Travis CI 2021-05-08 12:18:52 +02:00
f3ec810f8f Remove deploy from Travis CI 2021-05-08 11:49:45 +02:00
d13769f30f Use Travis CI to build the apk as a test 2021-05-08 11:47:35 +02:00
b5301a85f8 Add Travis CI and fix an analyze issue 2021-05-08 11:17:53 +02:00
6e215ff935 Define app bar brightness, increase minimum dart version and adapt comments to non-upgradable dependencies 2021-05-08 10:51:58 +02:00
dbe1604329 Adapt README, Switch to Stacked instead of outdated provider_architecture, Update dependencies 2021-05-07 21:02:13 +02:00
9b440ee63c Automatically switch to initial tab when coming from the share menu 2021-04-20 00:50:46 +02:00
c0a2e8d569 Add a slash to copied URLs, Expand item when pressing on the card's title in history view , release 1.3.2+11 2021-04-16 19:25:16 +02:00
06eb990eea Added gesture detection for tab bar, disable upload when no files have been attached or upload text input is empty, added version information to about view, minor refactor regarding state management, release 1.3.1+10 2021-04-06 14:00:00 +02:00
230de7fe40 Allow API key login, revamp profile view, adapt text color in tab bar, fix already listened in history view, minor refactor, release 1.3.0+9 2021-04-05 22:06:54 +02:00
c5da7ec84d Adapt status bar color to match app's theme, released 1.2.2+8 2021-04-04 10:44:31 +02:00
104 changed files with 4199 additions and 1602 deletions

6
.editorconfig Normal file
View file

@ -0,0 +1,6 @@
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

View file

@ -0,0 +1,21 @@
on: [ push ]
jobs:
build:
runs-on: docker
container:
image: ghcr.io/cirruslabs/flutter:3.24.4
steps:
- name: Prepare requirements
run: |
apt-get update
apt-get install -y nodejs npm git
rm -rf /var/lib/apt/lists/*
- uses: actions/checkout@v3
- name: Build
run: |
flutter doctor
flutter pub get
flutter pub outdated
flutter packages pub run build_runner build --delete-conflicting-outputs
flutter analyze --no-pub --no-current-package lib/
flutter build apk --debug

3
.gitignore vendored
View file

@ -1,6 +1,8 @@
# Miscellaneous # Miscellaneous
*.class *.class
*.lock *.lock
!Gemfile.lock
!pubspec.lock
*.log *.log
*.pyc *.pyc
*.swp *.swp
@ -16,6 +18,7 @@
*.iws *.iws
.idea/ .idea/
**/out/** **/out/**
.run/
# Visual Studio Code related # Visual Studio Code related
.vscode/ .vscode/

View file

@ -1,5 +1,85 @@
# CHANGELOG # CHANGELOG
## 1.6.4+22 - 2024/11/01
* Dependency updates
* Internal build updates
## 1.6.3+21
* Fixed not receiving share requests from other applications
## 1.6.2+20
* Updated internal dependencies
* Moved progress indicator of _Show Configuration_ into the underlying button
* Bumped Android minSdk to `30` (Android 11)
* Bumped Android targetSdk to `34` (Android 14)
* Fixed permission service not handling Android SDK 33 correctly
* Fixed permission service not being started during application start
## 1.6.1+19
* Updated internal dependencies
## 1.6.0+18
* Fixed input colors in login view when using dark theme
* Added removal of individual files selected for upload
* Added size for individual files selected for upload
* Replaced intent sharing library with `flutter_sharing_intent`
* Added proper linting to project
## 1.5.1+17
* Fixed white background button in AppBar when light theme enabled
* Cleaned up login screen and added icon
## 1.5.0+16
* Switched to Material You defaulting to blue swatch colors respecting dark mode
* Switched to Material You navigation bar and removed unsupported swipe navigation
* Increased target SDK to `33`
* Increased dart to `>= 2.18.5`
* Indicate configuration loading in profile view
* Switched linked git repository away from GitHub
* Updated internal dependencies
## 1.4.2+15
* Minor cleanup
* Added external drone CI
* Updated to Android embedding v2
* Updated to Gradle 7
* Upgraded internal dependencies to latest versions
## 1.4.1+14
* Fixed opening links
## 1.4.0+13
* Increased target SDK to `30`
* Upgraded to Dart `2.15.1`
* Upgraded to use null-safety
* Upgraded internal dependencies to latest versions
* Replaced `share` with `share_plus`
## 1.3.3+12
* Automatically switch to initial tab when coming from the share menu
* Upgraded internal dependencies
## 1.3.2+11
* Add a slash to copied URLs
* Expand item when pressing on the card's title in history view
## 1.3.1+10
* Added gesture detection for tab bar
* Disable upload when no files have been attached or upload text input is empty
* Added version information to about view
* Minor refactor regarding state management
## 1.3.0+9
* Allow API key login
* Revamp profile view
* Adapt color of tab bar text and use outlined icons when active
* Suffix the API key comment with UNIX timestamp when credential login is used
* Fixed an error when logging out and logging back in again in the history view
* Minor code refactor
## 1.2.2+8
* Adapt status bar color to match app's theme
## 1.2.1+7 ## 1.2.1+7
* Improve visual differences between the Upload tab and the upload button * Improve visual differences between the Upload tab and the upload button
* Improved visuals in bottom tab bar * Improved visuals in bottom tab bar

View file

@ -404,6 +404,14 @@ where to find the applicable terms.
form of a separately written license, or stated as exceptions; form of a separately written license, or stated as exceptions;
the above requirements apply either way. the above requirements apply either way.
7.1 Exceptions
7.1.1 As additional permission under section 7, you are allowed to distribute
the software through an app store, even if that store has restrictive
terms and conditions that are incompatible with the GPL, provided that the
source is also available under the GPL with or without this permission through
a channel without those restrictive terms and conditions.
8. Termination. 8. Termination.
You may not propagate or modify a covered work except as expressly You may not propagate or modify a covered work except as expressly

View file

@ -1,8 +1,14 @@
# README # README
A mobile flutter app for [FileBin](https://github.com/Bluewind/filebin). A mobile flutter app for [FileBin](https://git.server-speed.net/users/flo/filebin/).
Available on the [Play Store](https://play.google.com/store/apps/details?id=de.varakh.fbmobile). Available on the [Play Store](https://play.google.com/store/apps/details?id=de.varakh.fbmobile) and
[IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/de.varakh.fbmobile/).
The main git repository is hosted at **[https://git.myservermanager.com/varakh/fbmobile](https://git.myservermanager.com/varakh/fbmobile)**.
Other repositories are mirrors and pull requests, issues, and planning are managed there.
Contributions are very welcome!
## Getting Started ## Getting Started
@ -29,13 +35,6 @@ This project is a starting point for a Flutter application.
Start by installing dependencies and generating entities! Start by installing dependencies and generating entities!
### Working versions for SDK
```
[✓] Flutter (Channel stable, 1.22.6, on Linux, locale en_US.UTF-8)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
```
## Dependencies ## Dependencies
* Run `flutter packages pub get` in project root folder to get dependencies or open the `pubspec.yaml` and click on the buttons provided by the IDE plugins * Run `flutter packages pub get` in project root folder to get dependencies or open the `pubspec.yaml` and click on the buttons provided by the IDE plugins
@ -96,7 +95,7 @@ profiles. They're stored in a separate git repository and are encrypted.
You need access to the git repository in which those private files reside. You need access to the git repository in which those private files reside.
#### Usage #### Usage / doing the actual release
Go into the platform directory you want to build for, e.g. `ios/` or `android/` and then look into the Go into the platform directory you want to build for, e.g. `ios/` or `android/` and then look into the
`Fastlane` file which lanes are present. Run a lane via `fastlane <platform> <lane>`, e.g. use the `Fastlane` file which lanes are present. Run a lane via `fastlane <platform> <lane>`, e.g. use the
@ -106,13 +105,37 @@ following to build for Android `fastlane android build`.
##### Android ##### Android
Use `fastlane android beta` to build and upload a new beta version to the Play Store. It's recommended you set up `fastlane` via `bundler` (you need this to be installed on your machine).
Go into the `android/` sub-directory of the project
```shell
bundle config set --local path 'vendor/bundle'
bundle install
# update fastlane when needed
bundle update fastlane
# build only
bundle exec fastlane android build
# deploy (push BETA to app store)
bundle exec fastlane android beta
# deploy (push to app store)
bundle exec fastlane android deploy
# deploy (build signed fdroid large bundle [no target and abi split])
bundle exec fastlane android build_production_fdroid
```
##### iOS ##### iOS
For iOS you need to execute `fastlane ios build` before uploading to testflight with For iOS you need to execute `fastlane ios build` before uploading to testflight with
`fastlane ios beta`. `fastlane ios beta`.
Probably do the same Ruby/fastlane setup as mentioned under the _Android_ section.
### Release manually (not recommended) ### Release manually (not recommended)
See the following links on how to setup: See the following links on how to setup:

30
analysis_options.yaml Normal file
View file

@ -0,0 +1,30 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
library_private_types_in_public_api: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

2
android/.gitignore vendored
View file

@ -5,3 +5,5 @@ gradle-wrapper.jar
/gradlew.bat /gradlew.bat
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.bundle
vendor/

222
android/Gemfile.lock Normal file
View file

@ -0,0 +1,222 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.1000.0)
aws-sdk-core (3.211.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.169.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.225.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.7)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.5)
jwt (2.9.3)
base64
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
nanaimo (0.4.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (6.0.1)
rake (13.2.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.9)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
x86_64-linux
DEPENDENCIES
fastlane
BUNDLED WITH
2.5.16

View file

@ -1,3 +1,9 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode') def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) { if (flutterVersionCode == null) {
flutterVersionCode = '1' flutterVersionCode = '1'
@ -21,9 +22,6 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties') def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@ -31,7 +29,9 @@ if (keystorePropertiesFile.exists()) {
} }
android { android {
compileSdkVersion 29 compileSdkVersion 34
namespace "de.varakh.fbmobile"
lintOptions { lintOptions {
disable 'InvalidPackage' disable 'InvalidPackage'
@ -39,8 +39,8 @@ android {
defaultConfig { defaultConfig {
applicationId "de.varakh.fbmobile" applicationId "de.varakh.fbmobile"
minSdkVersion 16 minSdkVersion 30
targetSdkVersion 29 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -4,5 +4,16 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View file

@ -6,11 +6,12 @@
additional functionality it is fine to subclass or reimplement additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. --> FlutterApplication and put your custom class here. -->
<application <application
android:name="io.flutter.app.FlutterApplication" android:name="${applicationName}"
android:label="FileBin" android:label="FileBin"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@ -20,36 +21,31 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" /> <data android:mimeType="text/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -67,6 +63,18 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View file

@ -1,13 +1,6 @@
package de.varakh.fbmobile; package de.varakh.fbmobile;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity { public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
} }

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><!-- Modify this file to customize your launch splash screen -->
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" /> <item android:drawable="@android:color/white" />

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"> <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when <!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --> Flutter draws its first frame -->

View file

@ -4,5 +4,16 @@
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest> </manifest>

View file

@ -1,20 +1,7 @@
buildscript {
repositories {
google()
jcenter()
}
dependencies {
// TODO switch to 4.0.1 again: https://github.com/flutter/flutter/issues/58479
// TODO switch to 4.0.1 again: https://github.com/miguelpruivo/flutter_file_picker/issues/545
classpath 'com.android.tools.build:gradle:3.6.2'
}
}
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
} }
@ -26,6 +13,6 @@ subprojects {
project.evaluationDependsOn(':app') project.evaluationDependsOn(':app')
} }
task clean(type: Delete) { tasks.register("clean", Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }

View file

@ -11,6 +11,11 @@ platform :android do
sh("#{ENV['PWD']}/fastlane/buildAndroidProduction.sh") sh("#{ENV['PWD']}/fastlane/buildAndroidProduction.sh")
end end
desc "Build Production fdroid"
lane :build_production_fdroid do
sh("#{ENV['PWD']}/fastlane/buildAndroidProductionFdroid.sh")
end
desc "Build" desc "Build"
lane :build do lane :build do
sh("#{ENV['PWD']}/fastlane/buildAndroid.sh") sh("#{ENV['PWD']}/fastlane/buildAndroid.sh")

View file

@ -1,54 +1,80 @@
fastlane documentation fastlane documentation
================ ----
# Installation # Installation
Make sure you have the latest version of the Xcode command line tools installed: Make sure you have the latest version of the Xcode command line tools installed:
``` ```sh
xcode-select --install xcode-select --install
``` ```
Install _fastlane_ using For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew install fastlane`
# Available Actions # Available Actions
## Android ## Android
### android build_debug ### android build_debug
```sh
[bundle exec] fastlane android build_debug
``` ```
fastlane android build_debug
```
Build Debug Build Debug
### android build_production ### android build_production
```sh
[bundle exec] fastlane android build_production
``` ```
fastlane android build_production
```
Build Production Build Production
### android build_production_fdroid
```sh
[bundle exec] fastlane android build_production_fdroid
```
Build Production fdroid
### android build ### android build
```sh
[bundle exec] fastlane android build
``` ```
fastlane android build
```
Build Build
### android alpha ### android alpha
```sh
[bundle exec] fastlane android alpha
``` ```
fastlane android alpha
```
Deploy a new version to the Google Play as Alpha Deploy a new version to the Google Play as Alpha
### android beta ### android beta
```sh
[bundle exec] fastlane android beta
``` ```
fastlane android beta
```
Deploy a new version to the Google Play as Beta Deploy a new version to the Google Play as Beta
### android deploy ### android deploy
```sh
[bundle exec] fastlane android deploy
``` ```
fastlane android deploy
```
Deploy a new version to the Google Play Deploy a new version to the Google Play
---- ----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools). More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View file

@ -0,0 +1,9 @@
#!/usr/bin/env sh
cd ../../;
flutter clean && \
flutter pub get &&
flutter packages pub run build_runner build --delete-conflicting-outputs;
flutter build apk --release;
flutter build apk --split-per-abi --release;

View file

@ -1,4 +1,6 @@
agpVersion=8.7.2
kotlinVersion=1.7.10
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View file

@ -1,6 +1,7 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

View file

@ -1,15 +1,25 @@
include ':app' pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def plugins = new Properties() repositories {
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') google()
if (pluginsFile.exists()) { mavenCentral()
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } gradlePluginPortal()
}
} }
plugins.each { name, path -> plugins {
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() id "dev.flutter.flutter-plugin-loader" version "1.0.0"
include ":$name" id "com.android.application" version "${agpVersion}" apply false
project(":$name").projectDir = pluginDirectory id "org.jetbrains.kotlin.android" version "${kotlinVersion}" apply false
} }
include ":app"

View file

@ -18,12 +18,6 @@
"about": "About", "about": "About",
"upload": "Upload" "upload": "Upload"
}, },
"tabs": {
"login": "Login",
"history": "History",
"profile": "Profile",
"upload": "New"
},
"upload": { "upload": {
"and_or": "and/or", "and_or": "and/or",
"open_file_explorer": "Select file(s)...", "open_file_explorer": "Select file(s)...",
@ -45,12 +39,12 @@
"start_services": "Starting services..." "start_services": "Starting services..."
}, },
"login": { "login": {
"help": "Login",
"compatibility_dialog": { "compatibility_dialog": {
"title": "How to login?", "title": "How to login?",
"body": "A FileBin instance >= 3.5.0 and valid credentials for this instance are required." "body": "A FileBin instance >= 3.5.0 is required. Enter valid user and password or switch to API key login by clicking on the icons right next to this help icon."
}, },
"url_placeholder": "https://paste.domain.tld", "url_placeholder": "https://paste.domain.tld",
"apikey_placeholder": "API Key",
"username_placeholder": "Username", "username_placeholder": "Username",
"password_placeholder": "Password", "password_placeholder": "Password",
"button": "Login", "button": "Login",
@ -60,8 +54,10 @@
"invalid_url": "Please provide a valid FileBin URL", "invalid_url": "Please provide a valid FileBin URL",
"empty_username": "Please provide a username", "empty_username": "Please provide a username",
"empty_password": "Please provide a password", "empty_password": "Please provide a password",
"empty_apikey": "Please provide an API key",
"wrong_credentials": "Credentials are invalid", "wrong_credentials": "Credentials are invalid",
"forbidden": "You're not allowed to access this instance" "forbidden": "You're not allowed to access this instance",
"invalid_api_key": "You're not allowed to use this API key. Please verify that it's valid and at least has access level 'apikey'."
} }
}, },
"history": { "history": {
@ -91,22 +87,32 @@
} }
}, },
"about": { "about": {
"headline": "Welcome to FileBin mobile!", "versions": "{appName} ({packageName}) {version}+{buildNumber}",
"description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance.", "description": "This application is a mobile client for FileBin and it's open source. It helps you to manage your pastes.\n\nIn order to use the application, you need access to a FileBin instance.",
"faq_headline": "F.A.Q", "faq_headline": "F.A.Q",
"faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.", "faq": "- How do I login?\nInsert your instance URL and valid credentials you also use in the web interface of FileBin.\n\n- Why is storage permission required?\nIt's not required, but highly advised to grant it. Otherwise sharing files with the app won't work correctly and you might think that sharing has no effect.\n\n- When I am logged out, sharing files via share with the app won't list all files I selected after I login.\nPlease login before you start using the app. Account information are persisted. You only need to do it once.",
"contact_us": "Feedback? Issues?", "contact_us": "Feedback? Issues?",
"website": "Main application: https://github.com/Bluewind/filebin\n\nMobile: https://github.com/v4rakh/fbmobile" "website": "https://git.server-speed.net/users/flo/filebin and https://git.myservermanager.com/varakh/fbmobile"
}, },
"profile": { "profile": {
"welcome": "Hi!", "instance": "Instance",
"connection": "You're currently connected to:\n\nURL: {url}", "connection": "{url}",
"config": "Instance configuration:\n\nUpload max size: {uploadMaxSize}\n\nMax files per request: {maxFilesPerRequest}\n\nMax inputs vars: {maxInputVars}\n\nRequest max size: {requestMaxSize}", "show_config": "Show configuration",
"show_config_loading": "Loading configuration...",
"shown_config": {
"title": "Configuration",
"description": "Upload max size: {uploadMaxSize}\n\nMax files per request: {maxFilesPerRequest}\n\nMax inputs vars: {maxInputVars}\n\nRequest max size: {requestMaxSize}",
"error": {
"title": "Error",
"description": "An error occurred while loading the configuration values. Reason: {message}"
}
},
"reveal_api_key": "Reveal API key", "reveal_api_key": "Reveal API key",
"revealed_api_key": { "revealed_api_key": {
"title": "API key", "title": "API key",
"description": "{apiKey}" "description": "{apiKey}"
} },
"logout": "Logout"
}, },
"logout": { "logout": {
"title": "Logout", "title": "Logout",
@ -120,14 +126,6 @@
"description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types." "description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types."
} }
}, },
"permission_service": {
"dialog": {
"title": "Storage permission",
"description": "Storage permission should be granted to the app so that it can work properly. Do you want to grant permission or ignore this message permanently in the future?",
"grant": "Grant",
"ignore": "Ignore"
}
},
"dialog": { "dialog": {
"confirm": "OK", "confirm": "OK",
"cancel": "Cancel" "cancel": "Cancel"

View file

@ -1,190 +1,396 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
<!-- Created with Inkscape (http://www.inkscape.org/) --> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#"
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="128" height="128" id="svg2" version="1.1" inkscape:version="0.48.4 r9939" sodipodi:docname="FileBin.LOGO.6.FINAL.0.svg" inkscape:export-filename="/media/win2/projects/design/FileBin_(paste.xinu.at)_LOGO/FileBin.LOGO.6.FINAL.0.png" inkscape:export-xdpi="300" inkscape:export-ydpi="300"> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="128" height="128" id="svg2"
version="1.1" inkscape:version="0.48.4 r9939" sodipodi:docname="FileBin.LOGO.6.FINAL.0.svg"
inkscape:export-filename="/media/win2/projects/design/FileBin_(paste.xinu.at)_LOGO/FileBin.LOGO.6.FINAL.0.png"
inkscape:export-xdpi="300" inkscape:export-ydpi="300">
<defs id="defs4"> <defs id="defs4">
<linearGradient inkscape:collect="always" id="linearGradient13881"> <linearGradient inkscape:collect="always" id="linearGradient13881">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881" id="linearGradient13887" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-5" id="linearGradient13837-1" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> id="linearGradient13887" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-5"
id="linearGradient13837-1" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-5"> <linearGradient inkscape:collect="always" id="linearGradient13831-5">
<stop style="stop-color:#ef2929;stop-opacity:1" offset="0" id="stop13833-4" /> <stop style="stop-color:#ef2929;stop-opacity:1" offset="0" id="stop13833-4" />
<stop style="stop-color:#a40000;stop-opacity:1" offset="1" id="stop13835-1" /> <stop style="stop-color:#a40000;stop-opacity:1" offset="1" id="stop13835-1" />
</linearGradient> </linearGradient>
<linearGradient gradientTransform="translate(-0.7412829,-138.61258)" inkscape:collect="always" xlink:href="#linearGradient13831-4" id="linearGradient13837-6" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> <linearGradient gradientTransform="translate(-0.7412829,-138.61258)"
inkscape:collect="always" xlink:href="#linearGradient13831-4" id="linearGradient13837-6"
x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-4"> <linearGradient inkscape:collect="always" id="linearGradient13831-4">
<stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0" /> <stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0" />
<stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4" /> <stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient13887-0" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
id="linearGradient13887-0" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5"> <linearGradient inkscape:collect="always" id="linearGradient13881-5">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient13991" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient13999" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> id="linearGradient13991" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient14007" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient14015" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient14023" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> id="linearGradient13999" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14045" xlink:href="#linearGradient13881-5" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14047" xlink:href="#linearGradient13881-5" inkscape:collect="always"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14049" xlink:href="#linearGradient13881-5" inkscape:collect="always"/> id="linearGradient14007" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14051" xlink:href="#linearGradient13881-5" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14053" xlink:href="#linearGradient13881-5" inkscape:collect="always"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2" id="linearGradient13887-0-0" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> id="linearGradient14015" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
id="linearGradient14023" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14045"
xlink:href="#linearGradient13881-5" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14047"
xlink:href="#linearGradient13881-5" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14049"
xlink:href="#linearGradient13881-5" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14051"
xlink:href="#linearGradient13881-5" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14053"
xlink:href="#linearGradient13881-5" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2"
id="linearGradient13887-0-0" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-2"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-2">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5" />
</linearGradient> </linearGradient>
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14053-2" xlink:href="#linearGradient13881-5-2" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient gradientTransform="translate(502.01164,0.78745356)" inkscape:collect="always" xlink:href="#linearGradient13831-4-2-6" id="linearGradient13837-6-9-0" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> gradientUnits="userSpaceOnUse" id="linearGradient14053-2"
xlink:href="#linearGradient13881-5-2" inkscape:collect="always" />
<linearGradient gradientTransform="translate(502.01164,0.78745356)"
inkscape:collect="always" xlink:href="#linearGradient13831-4-2-6"
id="linearGradient13837-6-9-0" x1="128.57443" y1="886.22906" x2="128.57443"
y2="1012.7642" gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-4-2-6"> <linearGradient inkscape:collect="always" id="linearGradient13831-4-2-6">
<stop style="stop-color:#babdb6;stop-opacity:1" offset="0" id="stop13833-0-6-8" /> <stop style="stop-color:#babdb6;stop-opacity:1" offset="0" id="stop13833-0-6-8" />
<stop style="stop-color:#555753;stop-opacity:1" offset="1" id="stop13835-4-6-2" /> <stop style="stop-color:#555753;stop-opacity:1" offset="1" id="stop13835-4-6-2" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2" id="linearGradient13887-0-0-5" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2"
id="linearGradient13887-0-0-5" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1" />
</linearGradient> </linearGradient>
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14053-2-2" xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14045-3-2" xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" id="linearGradient14053-2-2"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14047-4-9" xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always"/> xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14049-8-7" xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14051-6-4" xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" id="linearGradient14045-3-2"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-2" id="linearGradient13887-0-0-5-9" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452" gradientUnits="userSpaceOnUse"/> xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14047-4-9"
xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14049-8-7"
xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14051-6-4"
xlink:href="#linearGradient13881-5-2-2" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-2"
id="linearGradient13887-0-0-5-9" x1="363.7587" y1="781.0882" x2="363.7587"
y2="270.32452" gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2-2"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2-2">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0-3" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0-3" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1-1" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1-1" />
</linearGradient> </linearGradient>
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14053-2-2-2" xlink:href="#linearGradient13881-5-2-2-2" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2" id="linearGradient14674" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> gradientUnits="userSpaceOnUse" id="linearGradient14053-2-2-2"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2" id="linearGradient14676" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> xlink:href="#linearGradient13881-5-2-2-2" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881" id="linearGradient14704" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881" id="linearGradient14706" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient14674" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient14724" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5" id="linearGradient14726" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2" id="linearGradient14728" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient14676" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2" id="linearGradient14730" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient gradientTransform="translate(502.01164,0.78745356)" inkscape:collect="always" xlink:href="#linearGradient13831-4-2-6-5" id="linearGradient13837-6-9-0-0" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881"
id="linearGradient14704" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881"
id="linearGradient14706" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
id="linearGradient14724" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5"
id="linearGradient14726" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2"
id="linearGradient14728" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2"
id="linearGradient14730" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient gradientTransform="translate(502.01164,0.78745356)"
inkscape:collect="always" xlink:href="#linearGradient13831-4-2-6-5"
id="linearGradient13837-6-9-0-0" x1="128.57443" y1="886.22906" x2="128.57443"
y2="1012.7642" gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-4-2-6-5"> <linearGradient inkscape:collect="always" id="linearGradient13831-4-2-6-5">
<stop style="stop-color:#babdb6;stop-opacity:1" offset="0" id="stop13833-0-6-8-6" /> <stop style="stop-color:#babdb6;stop-opacity:1" offset="0" id="stop13833-0-6-8-6" />
<stop style="stop-color:#555753;stop-opacity:1" offset="1" id="stop13835-4-6-2-5" /> <stop style="stop-color:#555753;stop-opacity:1" offset="1" id="stop13835-4-6-2-5" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-1" id="linearGradient14730-6" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-1"
id="linearGradient14730-6" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2-1"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-2-2-1">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0-0" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-0-0" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1-7" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-1-7" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-1" id="linearGradient14728-5" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-2-1"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14045-3-2-7" xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always"/> id="linearGradient14728-5" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14047-4-9-3" xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14049-8-7-2" xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14051-6-4-5" xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" id="linearGradient14045-3-2-7"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14800" xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always"/> xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-4" id="linearGradient14676-9" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14047-4-9-3"
xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14049-8-7-2"
xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14051-6-4-5"
xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14800"
xlink:href="#linearGradient13881-5-2-2-1" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-4"
id="linearGradient14676-9" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-2-4"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-2-4">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-00" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-8-00" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-7" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-5-7" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-4" id="linearGradient14674-9" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-2-4"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14963" xlink:href="#linearGradient13881-5-2-4" inkscape:collect="always"/> id="linearGradient14674-9" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient gradientTransform="translate(-0.7412829,-138.61258)" inkscape:collect="always" xlink:href="#linearGradient13831-4-3" id="linearGradient13837-6-1" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14963"
xlink:href="#linearGradient13881-5-2-4" inkscape:collect="always" />
<linearGradient gradientTransform="translate(-0.7412829,-138.61258)"
inkscape:collect="always" xlink:href="#linearGradient13831-4-3"
id="linearGradient13837-6-1" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"
gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-4-3"> <linearGradient inkscape:collect="always" id="linearGradient13831-4-3">
<stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-3" /> <stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-3" />
<stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4-0" /> <stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4-0" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27" id="linearGradient14726-7" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27"
id="linearGradient14726-7" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-27"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-27">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-9" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-9" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-54" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-54" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27" id="linearGradient14724-8" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14045-1" xlink:href="#linearGradient13881-5-27" inkscape:collect="always"/> id="linearGradient14724-8" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14047-0" xlink:href="#linearGradient13881-5-27" inkscape:collect="always"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14049-3" xlink:href="#linearGradient13881-5-27" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14051-1" xlink:href="#linearGradient13881-5-27" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" id="linearGradient14045-1"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15126" xlink:href="#linearGradient13881-5-27" inkscape:collect="always"/> xlink:href="#linearGradient13881-5-27" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-9" id="linearGradient14706-8" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14047-0"
xlink:href="#linearGradient13881-5-27" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14049-3"
xlink:href="#linearGradient13881-5-27" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14051-1"
xlink:href="#linearGradient13881-5-27" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15126"
xlink:href="#linearGradient13881-5-27" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-9"
id="linearGradient14706-8" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-9"> <linearGradient inkscape:collect="always" id="linearGradient13881-9">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-9" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-9" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-7" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-7" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-9" id="linearGradient14704-4" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-9"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15289" xlink:href="#linearGradient13881-9" inkscape:collect="always"/> id="linearGradient14704-4" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15289"
xlink:href="#linearGradient13881-9" inkscape:collect="always" />
<linearGradient id="linearGradient13831-4-27"> <linearGradient id="linearGradient13831-4-27">
<stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-9" /> <stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-9" />
<stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2" /> <stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient14726-0" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient14726-0" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-3"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-3">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient14724-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14110" xlink:href="#linearGradient13881-5-3" inkscape:collect="always"/> id="linearGradient14724-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27" id="linearGradient15013" gradientUnits="userSpaceOnUse" gradientTransform="translate(173.56054,-271.90218)" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15015" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15017" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> gradientUnits="userSpaceOnUse" id="linearGradient14110"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15019" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> xlink:href="#linearGradient13881-5-3" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15021" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15023" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15013" gradientUnits="userSpaceOnUse"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient15025" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> gradientTransform="translate(173.56054,-271.90218)" x1="128.57443" y1="886.22906"
<linearGradient gradientTransform="translate(-0.7412829,-138.61258)" inkscape:collect="always" xlink:href="#linearGradient13831-4-3-8" id="linearGradient13837-6-1-2" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642" gradientUnits="userSpaceOnUse"/> x2="128.57443" y2="1012.7642" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15015" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15017" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15019" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15021" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15023" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient15025" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient gradientTransform="translate(-0.7412829,-138.61258)"
inkscape:collect="always" xlink:href="#linearGradient13831-4-3-8"
id="linearGradient13837-6-1-2" x1="128.57443" y1="886.22906" x2="128.57443"
y2="1012.7642" gradientUnits="userSpaceOnUse" />
<linearGradient inkscape:collect="always" id="linearGradient13831-4-3-8"> <linearGradient inkscape:collect="always" id="linearGradient13831-4-3-8">
<stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-3-8" /> <stop style="stop-color:#729fcf;stop-opacity:1" offset="0" id="stop13833-0-3-8" />
<stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4-0-4" /> <stop style="stop-color:#3465a4;stop-opacity:1" offset="1" id="stop13835-4-0-4" />
</linearGradient> </linearGradient>
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15126-7" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15126-7"
xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-27-8"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-27-8">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-9-7" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-9-7" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-54-4" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-54-4" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27-8" id="linearGradient14724-8-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-27-8"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14045-1-0" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> id="linearGradient14724-8-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14047-0-5" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14049-3-2" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient14051-1-3" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> gradientUnits="userSpaceOnUse" id="linearGradient14045-1-0"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15095" xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always"/> xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14047-0-5"
xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14049-3-2"
xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient14051-1-3"
xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15095"
xlink:href="#linearGradient13881-5-27-8" inkscape:collect="always" />
<linearGradient id="linearGradient13831-4-27-2"> <linearGradient id="linearGradient13831-4-27-2">
<stop style="stop-color:#3465a4;stop-opacity:1;" offset="0" id="stop13833-0-9-6" /> <stop style="stop-color:#3465a4;stop-opacity:1;" offset="0" id="stop13833-0-9-6" />
<stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2-1" /> <stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2-1" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15025-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15025-3" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-3-6"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-3-6">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4-6" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4-6" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6-0" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6-0" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15023-5" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15260" xlink:href="#linearGradient13881-5-3-6" inkscape:collect="always"/> id="linearGradient15023-5" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27-2-5" id="linearGradient15370-4" gradientUnits="userSpaceOnUse" gradientTransform="translate(350.34367,-250.09554)" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15260"
xlink:href="#linearGradient13881-5-3-6" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27-2-5"
id="linearGradient15370-4" gradientUnits="userSpaceOnUse"
gradientTransform="translate(350.34367,-250.09554)" x1="128.57443" y1="886.22906"
x2="128.57443" y2="1012.7642" />
<linearGradient id="linearGradient13831-4-27-2-5"> <linearGradient id="linearGradient13831-4-27-2-5">
<stop style="stop-color:#3465a4;stop-opacity:1;" offset="0" id="stop13833-0-9-6-7" /> <stop style="stop-color:#3465a4;stop-opacity:1;" offset="0" id="stop13833-0-9-6-7" />
<stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2-1-3" /> <stop style="stop-color:#204a87;stop-opacity:1;" offset="1" id="stop13835-4-2-1-3" />
</linearGradient> </linearGradient>
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15260-9" xlink:href="#linearGradient13881-5-3-6-4" inkscape:collect="always"/> <linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15260-9"
xlink:href="#linearGradient13881-5-3-6-4" inkscape:collect="always" />
<linearGradient inkscape:collect="always" id="linearGradient13881-5-3-6-4"> <linearGradient inkscape:collect="always" id="linearGradient13881-5-3-6-4">
<stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4-6-5" /> <stop style="stop-color:#eeeeec;stop-opacity:1" offset="0" id="stop13883-7-4-6-5" />
<stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6-0-2" /> <stop style="stop-color:#ffffff;stop-opacity:1" offset="1" id="stop13885-3-6-0-2" />
</linearGradient> </linearGradient>
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4" id="linearGradient15023-5-0" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4" id="linearGradient15015-5-1" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15023-5-0" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4" id="linearGradient15017-6-4" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4" id="linearGradient15019-0-7" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4" id="linearGradient15021-5-6" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15015-5-1" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587" gradientUnits="userSpaceOnUse" id="linearGradient15457" xlink:href="#linearGradient13881-5-3-6-4" inkscape:collect="always"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27-2" id="linearGradient15552" gradientUnits="userSpaceOnUse" gradientTransform="translate(350.34367,-250.09554)" x1="128.57443" y1="886.22906" x2="128.57443" y2="1012.7642"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15554" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15017-6-4" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15556" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15558" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15560" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15019-0-7" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15562" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6" id="linearGradient15564" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> <linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6-4"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient3360" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> id="linearGradient15021-5-6" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3" id="linearGradient3362" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882" x2="363.7587" y2="270.32452"/> x2="363.7587" y2="270.32452" />
<linearGradient y2="270.32452" x2="363.7587" y1="781.0882" x1="363.7587"
gradientUnits="userSpaceOnUse" id="linearGradient15457"
xlink:href="#linearGradient13881-5-3-6-4" inkscape:collect="always" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13831-4-27-2"
id="linearGradient15552" gradientUnits="userSpaceOnUse"
gradientTransform="translate(350.34367,-250.09554)" x1="128.57443" y1="886.22906"
x2="128.57443" y2="1012.7642" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15554" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15556" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15558" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15560" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15562" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3-6"
id="linearGradient15564" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient3360" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
<linearGradient inkscape:collect="always" xlink:href="#linearGradient13881-5-3"
id="linearGradient3362" gradientUnits="userSpaceOnUse" x1="363.7587" y1="781.0882"
x2="363.7587" y2="270.32452" />
</defs> </defs>
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.4142136" inkscape:cx="116.01422" inkscape:cy="115.13684" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" showguides="true" inkscape:guide-bbox="true" inkscape:window-width="1231" inkscape:window-height="1138" inkscape:window-x="1920" inkscape:window-y="0" inkscape:window-maximized="0" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"> <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.4142136"
inkscape:cx="116.01422" inkscape:cy="115.13684" inkscape:document-units="px"
inkscape:current-layer="layer1" showgrid="false" showguides="true"
inkscape:guide-bbox="true" inkscape:window-width="1231" inkscape:window-height="1138"
inkscape:window-x="1920" inkscape:window-y="0" inkscape:window-maximized="0"
fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0">
<sodipodi:guide orientation="1,0" position="-178.89701,175.46456" id="guide13939" /> <sodipodi:guide orientation="1,0" position="-178.89701,175.46456" id="guide13939" />
<sodipodi:guide orientation="0,1" position="3.7515623,-42.079504" id="guide15364" /> <sodipodi:guide orientation="0,1" position="3.7515623,-42.079504" id="guide15364" />
<sodipodi:guide orientation="1,0" position="0.0010123365,156.27187" id="guide13111" /> <sodipodi:guide orientation="1,0" position="0.0010123365,156.27187" id="guide13111" />
@ -202,24 +408,55 @@
</cc:Work> </cc:Work>
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-231.22291,-886.63406)"> <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"
transform="translate(-231.22291,-886.63406)">
<g id="g14997" transform="matrix(0.99999368,0,0,0.99285775,-0.34584976,277.87803)"> <g id="g14997" transform="matrix(0.99999368,0,0,0.99285775,-0.34584976,277.87803)">
<rect ry="13.27135" y="615.24164" x="231.57103" height="126.81434" width="128" id="rect12980-4-1-8-0-8" style="color:#000000;fill:#183866;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> <rect ry="13.27135" y="615.24164" x="231.57103" height="126.81434" width="128"
<rect ry="13.395431" y="613.13519" x="231.57022" height="128" width="128" id="rect12980-4-1-3-02" style="color:#000000;fill:url(#linearGradient15013);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> id="rect12980-4-1-8-0-8"
<g style="opacity:0.3;fill:#000000" transform="matrix(0.25,0,0,0.25,202.56698,546.71978)" id="g14368-1-1-4-9-6"> style="color:#000000;fill:#183866;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect ry="13.395431" y="613.13519" x="231.57022" height="128" width="128"
id="rect12980-4-1-3-02"
style="color:#000000;fill:url(#linearGradient15013);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<g style="opacity:0.3;fill:#000000"
transform="matrix(0.25,0,0,0.25,202.56698,546.71978)" id="g14368-1-1-4-9-6">
<g style="fill:#000000" id="g14380-1-5-9-4-9"> <g style="fill:#000000" id="g14380-1-5-9-4-9">
<rect transform="matrix(1,0,-0.44619856,0.89493399,0,0)" ry="0" y="613.62866" x="498.49884" height="58.009731" width="246.91225" id="rect12978-93-3-97-3-4-8" style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> <rect transform="matrix(1,0,-0.44619856,0.89493399,0,0)" ry="0" y="613.62866"
<rect transform="matrix(1,0,-0.45382734,0.89108964,0,0)" ry="0" y="726.05896" x="554.76031" height="58.259998" width="320.02499" id="rect12978-9-6-44-8-0-2-6" style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> x="498.49884" height="58.009731" width="246.91225"
<rect transform="matrix(1,0,-0.44127854,0.89737018,0,0)" ry="0" y="502.93268" x="446.27722" height="57.852242" width="295.71799" id="rect12978-5-3-33-8-6-4-7" style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> id="rect12978-93-3-97-3-4-8"
<rect ry="0" rx="0" transform="matrix(1,0,-0.44219356,0.89691965,0,0)" y="394.08627" x="398.67178" height="57.881306" width="192.59586" id="rect12978-5-4-7-8-3-8-7-1" style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect transform="matrix(1,0,-0.45382734,0.89108964,0,0)" ry="0" y="726.05896"
x="554.76031" height="58.259998" width="320.02499"
id="rect12978-9-6-44-8-0-2-6"
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect transform="matrix(1,0,-0.44127854,0.89737018,0,0)" ry="0" y="502.93268"
x="446.27722" height="57.852242" width="295.71799"
id="rect12978-5-3-33-8-6-4-7"
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect ry="0" rx="0" transform="matrix(1,0,-0.44219356,0.89691965,0,0)"
y="394.08627" x="398.67178" height="57.881306" width="192.59586"
id="rect12978-5-4-7-8-3-8-7-1"
style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g> </g>
</g> </g>
<g style="fill:url(#linearGradient3362);fill-opacity:1" transform="matrix(0.25,0,0,0.25,202.55842,545.58997)" id="g14368-1-1-71-1"> <g style="fill:url(#linearGradient3362);fill-opacity:1"
transform="matrix(0.25,0,0,0.25,202.55842,545.58997)" id="g14368-1-1-71-1">
<g style="fill:url(#linearGradient3360);fill-opacity:1" id="g14380-1-5-08-8"> <g style="fill:url(#linearGradient3360);fill-opacity:1" id="g14380-1-5-08-8">
<rect transform="matrix(1,0,-0.44619856,0.89493399,0,0)" ry="0" y="613.62866" x="498.49884" height="58.009731" width="246.91225" id="rect12978-93-3-97-37-0" style="color:#000000;fill:url(#linearGradient15015);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> <rect transform="matrix(1,0,-0.44619856,0.89493399,0,0)" ry="0" y="613.62866"
<rect transform="matrix(1,0,-0.45382734,0.89108964,0,0)" ry="0" y="726.05896" x="554.76031" height="58.259998" width="320.02499" id="rect12978-9-6-44-8-46-5" style="color:#000000;fill:url(#linearGradient15017);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> x="498.49884" height="58.009731" width="246.91225"
<rect transform="matrix(1,0,-0.44127854,0.89737018,0,0)" ry="0" y="502.93268" x="446.27722" height="57.852242" width="295.71799" id="rect12978-5-3-33-8-33-29" style="color:#000000;fill:url(#linearGradient15019);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> id="rect12978-93-3-97-37-0"
<rect ry="0" rx="0" transform="matrix(1,0,-0.44219356,0.89691965,0,0)" y="394.08627" x="398.67178" height="57.881306" width="192.59586" id="rect12978-5-4-7-8-3-9-6" style="color:#000000;fill:url(#linearGradient15021);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"/> style="color:#000000;fill:url(#linearGradient15015);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect transform="matrix(1,0,-0.45382734,0.89108964,0,0)" ry="0" y="726.05896"
x="554.76031" height="58.259998" width="320.02499"
id="rect12978-9-6-44-8-46-5"
style="color:#000000;fill:url(#linearGradient15017);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect transform="matrix(1,0,-0.44127854,0.89737018,0,0)" ry="0" y="502.93268"
x="446.27722" height="57.852242" width="295.71799"
id="rect12978-5-3-33-8-33-29"
style="color:#000000;fill:url(#linearGradient15019);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect ry="0" rx="0" transform="matrix(1,0,-0.44219356,0.89691965,0,0)"
y="394.08627" x="398.67178" height="57.881306" width="192.59586"
id="rect12978-5-4-7-8-3-9-6"
style="color:#000000;fill:url(#linearGradient15021);fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:0;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:79.67999581;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
</g> </g>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -1 +1,204 @@
{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]} {
"images": [
{
"size": "60x60",
"expected-size": "180",
"filename": "180.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "120",
"filename": "120.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "60x60",
"expected-size": "120",
"filename": "120.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "57x57",
"expected-size": "57",
"filename": "57.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "87",
"filename": "87.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "57x57",
"expected-size": "114",
"filename": "114.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "60",
"filename": "60.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "iphone",
"scale": "3x"
},
{
"size": "1024x1024",
"filename": "1024.png",
"expected-size": "1024",
"idiom": "ios-marketing",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"scale": "1x"
},
{
"size": "40x40",
"expected-size": "80",
"filename": "80.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "72x72",
"expected-size": "72",
"filename": "72.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "76x76",
"expected-size": "152",
"filename": "152.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "50x50",
"expected-size": "100",
"filename": "100.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "29x29",
"expected-size": "58",
"filename": "58.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "76x76",
"expected-size": "76",
"filename": "76.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "29x29",
"expected-size": "29",
"filename": "29.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "50x50",
"expected-size": "50",
"filename": "50.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "72x72",
"expected-size": "144",
"filename": "144.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "40x40",
"expected-size": "40",
"filename": "40.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "83.5x83.5",
"expected-size": "167",
"filename": "167.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
},
{
"size": "20x20",
"expected-size": "20",
"filename": "20.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "1x"
},
{
"size": "20x20",
"expected-size": "40",
"filename": "40.png",
"folder": "Assets.xcassets/AppIcon.appiconset/",
"idiom": "ipad",
"scale": "2x"
}
]
}

View file

@ -60,8 +60,13 @@
</dict> </dict>
<dict/> <dict/>
</array> </array>
// TODO follow steps 2) on create share extension (https://pub.dev/packages/receive_sharing_intent) // TODO follow steps on create share extension (https://pub.dev/packages/flutter_sharing_intent)
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Allow to select photos and upload them via the app</string> <string>Allow to select photos and upload them via the app</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -1,7 +1,7 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:flutter_translate/localization_provider.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:flutter_translate/localized_app.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'core/enums/refresh_event.dart'; import 'core/enums/refresh_event.dart';
@ -18,33 +18,54 @@ import 'ui/shared/app_colors.dart';
import 'ui/views/startup_view.dart'; import 'ui/views/startup_view.dart';
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
static final _defaultLightColorScheme = ColorScheme.fromSwatch(
primarySwatch: myColor, brightness: Brightness.light);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
primarySwatch: myColor, brightness: Brightness.dark);
MyApp({super.key}) {
initializeDateFormatting('en');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var localizationDelegate = LocalizedApp.of(context).delegate; var localizationDelegate = LocalizedApp.of(context).delegate;
return LocalizationProvider( return LocalizationProvider(
state: LocalizationProvider.of(context).state, state: LocalizationProvider.of(context).state,
child: StreamProvider<RefreshEvent>( child: StreamProvider<RefreshEvent?>(
initialData: null, initialData: null,
create: (context) => locator<RefreshService>().refreshHistoryController.stream, create: (context) =>
child: StreamProvider<Session>( locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session?>(
initialData: Session.initial(), initialData: Session.initial(),
create: (context) => locator<SessionService>().sessionController.stream, create: (context) =>
child: LifeCycleManager( locator<SessionService>().sessionController.stream,
child: MaterialApp( child: LifeCycleManager(child: DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: translate('app.title'), title: translate('app.title'),
builder: (context, child) => Navigator( builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey, key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)), onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
), ),
theme: ThemeData( theme: ThemeData(
brightness: Brightness.light, primarySwatch: primaryAccentColor, primaryColor: primaryAccentColor), useMaterial3: true,
brightness: Brightness.light,
colorScheme:
lightColorScheme ?? _defaultLightColorScheme),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
onGenerateRoute: AppRouter.generateRoute, onGenerateRoute: AppRouter.generateRoute,
navigatorKey: locator<NavigationService>().navigationKey, navigatorKey: locator<NavigationService>().navigationKey,
home: StartUpView(), home: const StartUpView(),
supportedLocales: localizationDelegate.supportedLocales, supportedLocales: localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale, locale: localizationDelegate.currentLocale,
)), );
})),
))); )));
} }
} }

View file

@ -1,8 +1,8 @@
class DialogRequest { class DialogRequest {
final String title; final String? title;
final String description; final String? description;
final String buttonTitleAccept; final String? buttonTitleAccept;
final String buttonTitleDeny; final String? buttonTitleDeny;
DialogRequest({ DialogRequest({
this.title, this.title,

View file

@ -1,5 +1,5 @@
class DialogResponse { class DialogResponse {
final bool confirmed; final bool? confirmed;
DialogResponse({ DialogResponse({
this.confirmed, this.confirmed,

View file

@ -1,12 +1,15 @@
/// Enums for error codes /// Enums for error codes
enum ErrorCode { enum ErrorCode {
/// A generic error /// A generic error
GENERAL_ERROR, generalError,
/// Errors related to connections /// Errors related to connections
SOCKET_ERROR, socketError,
SOCKET_TIMEOUT, socketTimeout,
/// A REST error (response code wasn't 200 or 204) /// A REST error (response code wasn't 200 or 204)
REST_ERROR, restError,
/// Custom errors
invalidApiKey
} }

View file

@ -1 +1 @@
enum RefreshEvent { RefreshHistory } enum RefreshEvent { refreshHistory }

View file

@ -1 +1 @@
enum ViewState { Idle, Busy } enum ViewState { idle, busy }

View file

@ -5,9 +5,10 @@ class RestServiceException extends ServiceException {
final int statusCode; final int statusCode;
final dynamic responseBody; final dynamic responseBody;
RestServiceException(this.statusCode, {this.responseBody, String message}) RestServiceException(this.statusCode, {this.responseBody, super.message = null})
: super(code: ErrorCode.REST_ERROR, message: message); : super(code: ErrorCode.restError);
@override
String toString() { String toString() {
return "$code $statusCode $message"; return "$code $statusCode $message";
} }

View file

@ -2,10 +2,11 @@ import '../enums/error_code.dart';
class ServiceException implements Exception { class ServiceException implements Exception {
final ErrorCode code; final ErrorCode code;
final String message; final String? message;
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''}); ServiceException({this.code = ErrorCode.generalError, this.message = ''});
@override
String toString() { String toString() {
return "$code: $message"; return "$code: $message";
} }

View file

@ -6,15 +6,16 @@ import '../datamodels/dialog_response.dart';
import '../services/dialog_service.dart'; import '../services/dialog_service.dart';
class DialogManager extends StatefulWidget { class DialogManager extends StatefulWidget {
final Widget child; final Widget? child;
DialogManager({Key key, this.child}) : super(key: key); const DialogManager({super.key, this.child});
@override
_DialogManagerState createState() => _DialogManagerState(); _DialogManagerState createState() => _DialogManagerState();
} }
class _DialogManagerState extends State<DialogManager> { class _DialogManagerState extends State<DialogManager> {
DialogService _dialogService = locator<DialogService>(); final DialogService _dialogService = locator<DialogService>();
@override @override
void initState() { void initState() {
@ -24,15 +25,16 @@ class _DialogManagerState extends State<DialogManager> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return widget.child!;
} }
void _showDialog(DialogRequest request) { void _showDialog(DialogRequest request) {
List<Widget> actions = <Widget>[]; List<Widget> actions = <Widget>[];
if (request.buttonTitleDeny != null && request.buttonTitleDeny.isNotEmpty) { if (request.buttonTitleDeny != null &&
request.buttonTitleDeny!.isNotEmpty) {
Widget denyBtn = TextButton( Widget denyBtn = TextButton(
child: Text(request.buttonTitleDeny), child: Text(request.buttonTitleDeny!),
onPressed: () { onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: false)); _dialogService.dialogComplete(DialogResponse(confirmed: false));
}, },
@ -41,7 +43,7 @@ class _DialogManagerState extends State<DialogManager> {
} }
Widget confirmBtn = TextButton( Widget confirmBtn = TextButton(
child: Text(request.buttonTitleAccept), child: Text(request.buttonTitleAccept!),
onPressed: () { onPressed: () {
_dialogService.dialogComplete(DialogResponse(confirmed: true)); _dialogService.dialogComplete(DialogResponse(confirmed: true));
}, },
@ -49,8 +51,8 @@ class _DialogManagerState extends State<DialogManager> {
actions.add(confirmBtn); actions.add(confirmBtn);
AlertDialog alert = AlertDialog( AlertDialog alert = AlertDialog(
title: Text(request.title), title: Text(request.title!),
content: Text(request.description), content: Text(request.description!),
actions: actions, actions: actions,
); );

View file

@ -9,21 +9,26 @@ import '../util/logger.dart';
/// Stop and start long running services /// Stop and start long running services
class LifeCycleManager extends StatefulWidget { class LifeCycleManager extends StatefulWidget {
final Widget child; final Widget? child;
LifeCycleManager({Key key, this.child}) : super(key: key); const LifeCycleManager({super.key, this.child});
@override
_LifeCycleManagerState createState() => _LifeCycleManagerState(); _LifeCycleManagerState createState() => _LifeCycleManagerState();
} }
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver { class _LifeCycleManagerState extends State<LifeCycleManager>
with WidgetsBindingObserver {
final Logger logger = getLogger(); final Logger logger = getLogger();
List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()]; List<StoppableService> servicesToManage = [
locator<SessionService>(),
locator<PermissionService>()
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.child; return widget.child!;
} }
@override @override
@ -43,12 +48,12 @@ class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBinding
logger.d('LifeCycle event ${state.toString()}'); logger.d('LifeCycle event ${state.toString()}');
super.didChangeAppLifecycleState(state); super.didChangeAppLifecycleState(state);
servicesToManage.forEach((service) { for (var service in servicesToManage) {
if (state == AppLifecycleState.resumed) { if (state == AppLifecycleState.resumed) {
service.start(); service.start();
} else { } else {
service.stop(); service.stop();
} }
}); }
} }
} }

View file

@ -0,0 +1,29 @@
import 'package:json_annotation/json_annotation.dart';
part 'apikey.g.dart';
@JsonSerializable()
class ApiKey {
@JsonKey(required: true)
final String key;
@JsonKey(required: true)
final String created;
@JsonKey(required: true, name: 'access_level')
final String accessLevel;
final String? comment;
ApiKey(
{required this.key,
required this.created,
required this.accessLevel,
this.comment});
// JSON Init
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeyToJson(this);
}

View file

@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
import 'apikey.dart';
part 'apikeys.g.dart';
@JsonSerializable()
class ApiKeys {
@JsonKey(name: "items", required: true)
final Map<String, ApiKey> apikeys;
ApiKeys({required this.apikeys});
// JSON Init
factory ApiKeys.fromJson(Map<String, dynamic> json) =>
_$ApiKeysFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);
}

View file

@ -0,0 +1,23 @@
import 'package:json_annotation/json_annotation.dart';
import 'apikeys.dart';
part 'apikeys_response.g.dart';
@JsonSerializable()
class ApiKeysResponse {
@JsonKey(required: true)
final String status;
@JsonKey(required: true)
final ApiKeys data;
ApiKeysResponse({required this.status, required this.data});
// JSON Init
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) =>
_$ApiKeysResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);
}

View file

@ -16,7 +16,11 @@ class Config {
@JsonKey(name: "request_max_size", required: true) @JsonKey(name: "request_max_size", required: true)
final num requestMaxSize; final num requestMaxSize;
Config({this.uploadMaxSize, this.maxFilesPerRequest, this.maxInputVars, this.requestMaxSize}); Config(
{required this.uploadMaxSize,
required this.maxFilesPerRequest,
required this.maxInputVars,
required this.requestMaxSize});
// JSON Init // JSON Init
factory Config.fromJson(Map<String, dynamic> json) => _$ConfigFromJson(json); factory Config.fromJson(Map<String, dynamic> json) => _$ConfigFromJson(json);

View file

@ -12,10 +12,11 @@ class ConfigResponse {
@JsonKey(required: true) @JsonKey(required: true)
final Config data; final Config data;
ConfigResponse({this.status, this.data}); ConfigResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json); factory ConfigResponse.fromJson(Map<String, dynamic> json) =>
_$ConfigResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this); Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);

View file

@ -10,10 +10,11 @@ class CreateApiKeyResponse {
@JsonKey(required: true) @JsonKey(required: true)
final Map<String, String> data; final Map<String, String> data;
CreateApiKeyResponse({this.status, this.data}); CreateApiKeyResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json); factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) =>
_$CreateApiKeyResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this); Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);

View file

@ -14,12 +14,13 @@ class History {
final Map<String, HistoryMultipasteItem> multipasteItems; final Map<String, HistoryMultipasteItem> multipasteItems;
@JsonKey(name: "total_size") @JsonKey(name: "total_size")
final String totalSize; final String? totalSize;
History({this.items, this.multipasteItems, this.totalSize}); History({required this.items, required this.multipasteItems, this.totalSize});
// JSON Init // JSON Init
factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json); factory History.fromJson(Map<String, dynamic> json) =>
_$HistoryFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryToJson(this); Map<String, dynamic> toJson() => _$HistoryToJson(this);

View file

@ -11,12 +11,20 @@ class HistoryItem {
final String filesize; final String filesize;
final String hash; final String hash;
final String mimetype; final String mimetype;
final String thumbnail; final String? thumbnail;
HistoryItem({this.date, this.filename, this.filesize, this.hash, this.id, this.mimetype, this.thumbnail}); HistoryItem(
{required this.date,
required this.filename,
required this.filesize,
required this.hash,
required this.id,
required this.mimetype,
this.thumbnail});
// JSON Init // JSON Init
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json); factory HistoryItem.fromJson(Map<String, dynamic> json) =>
_$HistoryItemFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryItemToJson(this); Map<String, dynamic> toJson() => _$HistoryItemToJson(this);

View file

@ -1,21 +1,22 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'history_item.dart'; import 'history_multipaste_item_entry.dart';
part 'history_multipaste_item.g.dart'; part 'history_multipaste_item.g.dart';
@JsonSerializable() @JsonSerializable()
class HistoryMultipasteItem { class HistoryMultipasteItem {
final String date; final String date;
final Map<String, HistoryItem> items; final Map<String, HistoryMultipasteItemEntry> items;
@JsonKey(name: "url_id") @JsonKey(name: "url_id")
final String urlId; final String urlId;
HistoryMultipasteItem({this.date, this.items, this.urlId}); HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
// JSON Init // JSON Init
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json); factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this); Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);

View file

@ -0,0 +1,17 @@
import 'package:json_annotation/json_annotation.dart';
part 'history_multipaste_item_entry.g.dart';
@JsonSerializable()
class HistoryMultipasteItemEntry {
final String id;
HistoryMultipasteItemEntry({required this.id});
// JSON Init
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemEntryFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);
}

View file

@ -12,10 +12,11 @@ class HistoryResponse {
@JsonKey(required: true) @JsonKey(required: true)
final History data; final History data;
HistoryResponse({this.status, this.data}); HistoryResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json); factory HistoryResponse.fromJson(Map<String, dynamic> json) =>
_$HistoryResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this); Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);

View file

@ -10,12 +10,13 @@ class RestError {
final String errorId; final String errorId;
RestError({ RestError({
this.status, required this.status,
this.message, required this.message,
this.errorId, required this.errorId,
}); // JSON Init }); // JSON Init
factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json); factory RestError.fromJson(Map<String, dynamic> json) =>
_$RestErrorFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$RestErrorToJson(this); Map<String, dynamic> toJson() => _$RestErrorToJson(this);

View file

@ -10,10 +10,11 @@ class Uploaded {
@JsonKey(required: true) @JsonKey(required: true)
final List<String> urls; final List<String> urls;
Uploaded({this.ids, this.urls}); Uploaded({required this.ids, required this.urls});
// JSON Init // JSON Init
factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json); factory Uploaded.fromJson(Map<String, dynamic> json) =>
_$UploadedFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedToJson(this); Map<String, dynamic> toJson() => _$UploadedToJson(this);

View file

@ -10,10 +10,11 @@ class UploadedMulti {
@JsonKey(required: true, name: "url_id") @JsonKey(required: true, name: "url_id")
final String urlId; final String urlId;
UploadedMulti({this.url, this.urlId}); UploadedMulti({required this.url, required this.urlId});
// JSON Init // JSON Init
factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json); factory UploadedMulti.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this); Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);

View file

@ -12,10 +12,11 @@ class UploadedMultiResponse {
@JsonKey(required: true) @JsonKey(required: true)
final UploadedMulti data; final UploadedMulti data;
UploadedMultiResponse({this.status, this.data}); UploadedMultiResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json); factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this); Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);

View file

@ -12,10 +12,11 @@ class UploadedResponse {
@JsonKey(required: true) @JsonKey(required: true)
final Uploaded data; final Uploaded data;
UploadedResponse({this.status, this.data}); UploadedResponse({required this.status, required this.data});
// JSON Init // JSON Init
factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json); factory UploadedResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedResponseFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this); Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);

View file

@ -1,23 +1,20 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'rest/config.dart';
part 'session.g.dart'; part 'session.g.dart';
@JsonSerializable() @JsonSerializable()
class Session { class Session {
final String url; final String url;
final String apiKey; final String apiKey;
final Config config;
Session({this.url, this.apiKey, this.config}); Session({required this.url, required this.apiKey});
Session.initial() Session.initial()
: url = '', : url = '',
apiKey = '', apiKey = '';
config = null;
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json); factory Session.fromJson(Map<String, dynamic> json) =>
_$SessionFromJson(json);
Map<String, dynamic> toJson() => _$SessionToJson(this); Map<String, dynamic> toJson() => _$SessionToJson(this);
} }

View file

@ -4,29 +4,30 @@ part 'uploaded_paste.g.dart';
@JsonSerializable() @JsonSerializable()
class UploadedPaste { class UploadedPaste {
final DateTime date; final DateTime? date;
final String filename; final String? filename;
final num filesize; final num? filesize;
final String hash; final String? hash;
final String id; final String id;
final String mimetype; final String? mimetype;
final String thumbnail; final String? thumbnail;
final bool isMulti; final bool? isMulti;
final List<String> items; final List<String?>? items;
UploadedPaste( UploadedPaste(
{this.date, {this.date,
this.filename, this.filename,
this.filesize, this.filesize,
this.hash, this.hash,
this.id, required this.id,
this.mimetype, this.mimetype,
this.thumbnail, this.thumbnail,
this.isMulti, this.isMulti,
this.items}); this.items});
// JSON Init // JSON Init
factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json); factory UploadedPaste.fromJson(Map<String, dynamic> json) =>
_$UploadedPasteFromJson(json);
// JSON Export // JSON Export
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this); Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);

View file

@ -11,7 +11,7 @@ import '../models/rest/uploaded_response.dart';
import '../services/api.dart'; import '../services/api.dart';
class FileRepository { class FileRepository {
Api _api = locator<Api>(); final Api _api = locator<Api>();
Future<History> getHistory() async { Future<History> getHistory() async {
var response = await _api.post('/file/history'); var response = await _api.post('/file/history');
@ -27,23 +27,29 @@ class FileRepository {
return parsedResponse.data; return parsedResponse.data;
} }
Future delete(String id) async { Future postDelete(String id) async {
await _api.post('/file/delete', fields: {'ids[1]': id}); var fields = Map.fromEntries([MapEntry("ids[1]", id)]);
var response = await _api.post('/file/delete', fields: fields);
return response;
} }
Future<UploadedResponse> upload(List<File> files, Map<String, String> additionalFiles) async { Future<UploadedResponse> postUpload(
var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles); List<File>? files, Map<String, String>? additionalFiles) async {
var response = await _api.post('/file/upload',
files: files, additionalFiles: additionalFiles);
return UploadedResponse.fromJson(json.decode(response.body)); return UploadedResponse.fromJson(json.decode(response.body));
} }
Future createMulti(List<String> ids) async { Future<UploadedMultiResponse> postCreateMultiPaste(List<String> ids) async {
Map<String, String> multiPasteIds = Map(); Map<String, String> multiPasteIds = {};
ids.forEach((element) { for (var element in ids) {
multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element); multiPasteIds.putIfAbsent(
}); "ids[${ids.indexOf(element) + 1}]", () => element);
}
var response = await _api.post('/file/create_multipaste', fields: multiPasteIds); var response =
await _api.post('/file/create_multipaste', fields: multiPasteIds);
return UploadedMultiResponse.fromJson(json.decode(response.body)); return UploadedMultiResponse.fromJson(json.decode(response.body));
} }
} }

View file

@ -1,18 +1,29 @@
import 'dart:convert'; import 'dart:convert';
import '../../locator.dart'; import '../../locator.dart';
import '../models/rest/apikeys_response.dart';
import '../models/rest/create_apikey_response.dart'; import '../models/rest/create_apikey_response.dart';
import '../services/api.dart'; import '../services/api.dart';
class UserRepository { class UserRepository {
Api _api = locator<Api>(); final Api _api = locator<Api>();
Future<CreateApiKeyResponse> createApiKey( Future<CreateApiKeyResponse> postApiKey(String url, String username,
String url, String username, String password, String accessLevel, String comment) async { String password, String accessLevel, String comment) async {
_api.setUrl(url); _api.setUrl(url);
var response = await _api.post('/user/create_apikey', var fields = Map.fromEntries([
fields: {'username': username, 'password': password, 'access_level': accessLevel, 'comment': comment}); MapEntry("username", username),
MapEntry("password", password),
MapEntry("access_level", accessLevel),
MapEntry("comment", comment),
]);
var response = await _api.post('/user/create_apikey', fields: fields);
return CreateApiKeyResponse.fromJson(json.decode(response.body)); return CreateApiKeyResponse.fromJson(json.decode(response.body));
} }
Future<ApiKeysResponse> getApiKeys() async {
var response = await _api.post('/user/apikeys');
return ApiKeysResponse.fromJson(json.decode(response.body));
}
} }

View file

@ -24,25 +24,34 @@ class Api implements ApiErrorConverter {
String _url = ""; String _url = "";
String _apiKey = ""; String _apiKey = "";
Map<String, String> _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson}; final Map<String, String> _headers = {
Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit); "Content-Type": _applicationJson,
"Accept": _applicationJson
};
Duration _timeout = const Duration(seconds: Constants.apiRequestTimeoutLimit);
Future<http.Response> fetch<T>(String route) async { Future<http.Response> fetch<T>(String route) async {
try { try {
_logger _logger.d(
.d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'"); "Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
var response = await http.get(_url + route, headers: _headers).timeout(_timeout); var response = await http
.get(Uri.parse(_url + route), headers: _headers)
.timeout(_timeout);
handleRestErrors(response); handleRestErrors(response);
return response; return response;
} on TimeoutException { } on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); throw ServiceException(
code: ErrorCode.socketTimeout, message: _errorTimeout);
} on SocketException { } on SocketException {
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection); throw ServiceException(
code: ErrorCode.socketError, message: _errorNoConnection);
} }
} }
Future<http.Response> post<T>(String route, Future<http.Response> post<T>(String route,
{Map<String, String> fields, List<File> files, Map<String, String> additionalFiles}) async { {Map<String, String?>? fields,
List<File>? files,
Map<String, String>? additionalFiles}) async {
try { try {
var uri = Uri.parse(_url + route); var uri = Uri.parse(_url + route);
var request = http.MultipartRequest('POST', uri) var request = http.MultipartRequest('POST', uri)
@ -54,32 +63,39 @@ class Api implements ApiErrorConverter {
} }
if (fields != null && fields.isNotEmpty) { if (fields != null && fields.isNotEmpty) {
request.fields.addAll(fields); request.fields.addAll(fields as Map<String, String>);
} }
if (files != null && files.isNotEmpty) { if (files != null && files.isNotEmpty) {
files.forEach((element) async { for (var element in files) {
request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path)); request.files.add(await http.MultipartFile.fromPath(
}); 'file[${files.indexOf(element) + 1}]', element.path));
}
} }
if (additionalFiles != null && additionalFiles.length > 0) { if (additionalFiles != null && additionalFiles.isNotEmpty) {
List<String> keys = additionalFiles.keys.toList(); List<String> keys = additionalFiles.keys.toList();
additionalFiles.forEach((key, value) { additionalFiles.forEach((key, value) {
var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1; var index = files != null
request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key)); ? files.length + keys.indexOf(key) + 1
: keys.indexOf(key) + 1;
request.files.add(http.MultipartFile.fromString('file[$index]', value,
filename: key));
}); });
} }
_logger.d("Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files"); _logger.d(
"Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
var multiResponse = await request.send(); var multiResponse = await request.send();
var response = await http.Response.fromStream(multiResponse); var response = await http.Response.fromStream(multiResponse);
handleRestErrors(response); handleRestErrors(response);
return response; return response;
} on TimeoutException { } on TimeoutException {
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout); throw ServiceException(
code: ErrorCode.socketTimeout, message: _errorTimeout);
} on SocketException { } on SocketException {
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection); throw ServiceException(
code: ErrorCode.socketError, message: _errorNoConnection);
} }
} }
@ -107,20 +123,21 @@ class Api implements ApiErrorConverter {
/// have a json decoded object. Replace this with a custom /// have a json decoded object. Replace this with a custom
/// conversion method by overwriting the interface if needed /// conversion method by overwriting the interface if needed
void handleRestErrors(http.Response response) { void handleRestErrors(http.Response response) {
if (response != null) { if (response.statusCode != HttpStatus.ok &&
if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) { response.statusCode != HttpStatus.noContent) {
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) { if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
ContentType responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]); ContentType responseContentType =
ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
if (ContentType.json.primaryType == responseContentType.primaryType && if (ContentType.json.primaryType == responseContentType.primaryType &&
ContentType.json.subType == responseContentType.subType) { ContentType.json.subType == responseContentType.subType) {
var parsedBody = convert(response); var parsedBody = convert(response);
throw new RestServiceException(response.statusCode, responseBody: parsedBody); throw RestServiceException(response.statusCode,
responseBody: parsedBody);
} }
} }
throw new RestServiceException(response.statusCode); throw RestServiceException(response.statusCode);
}
} }
} }

View file

@ -7,9 +7,10 @@ import '../datamodels/dialog_request.dart';
import '../datamodels/dialog_response.dart'; import '../datamodels/dialog_response.dart';
class DialogService { class DialogService {
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> _dialogNavigationKey =
Function(DialogRequest) _showDialogListener; GlobalKey<NavigatorState>();
Completer<DialogResponse> _dialogCompleter; late Function(DialogRequest) _showDialogListener;
Completer<DialogResponse>? _dialogCompleter;
GlobalKey<NavigatorState> get dialogNavigationKey => _dialogNavigationKey; GlobalKey<NavigatorState> get dialogNavigationKey => _dialogNavigationKey;
@ -18,35 +19,43 @@ class DialogService {
} }
Future<DialogResponse> showDialog({ Future<DialogResponse> showDialog({
String title, String? title,
String description, String? description,
String buttonTitleAccept, String? buttonTitleAccept,
}) { }) {
_dialogCompleter = Completer<DialogResponse>(); _dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest( _showDialogListener(DialogRequest(
title: title, title: title,
description: description, description: description,
buttonTitleAccept: buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept)); buttonTitleAccept == null || buttonTitleAccept.isEmpty
return _dialogCompleter.future; ? translate('dialog.confirm')
: buttonTitleAccept));
return _dialogCompleter!.future;
} }
Future<DialogResponse> showConfirmationDialog( Future<DialogResponse> showConfirmationDialog(
{String title, String description, String buttonTitleAccept, String buttonTitleDeny}) { {String? title,
String? description,
String? buttonTitleAccept,
String? buttonTitleDeny}) {
_dialogCompleter = Completer<DialogResponse>(); _dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest( _showDialogListener(DialogRequest(
title: title, title: title,
description: description, description: description,
buttonTitleAccept: buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept, buttonTitleAccept == null || buttonTitleAccept.isEmpty
buttonTitleDeny: ? translate('dialog.confirm')
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny)); : buttonTitleAccept,
return _dialogCompleter.future; buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
? translate('dialog.cancel')
: buttonTitleDeny));
return _dialogCompleter!.future;
} }
void dialogComplete(DialogResponse response) { void dialogComplete(DialogResponse response) {
_dialogNavigationKey.currentState.pop(); _dialogNavigationKey.currentState!.pop();
_dialogCompleter.complete(response); _dialogCompleter!.complete(response);
_dialogCompleter = null; _dialogCompleter = null;
} }
} }

View file

@ -3,27 +3,32 @@ import 'dart:io';
import '../../core/repositories/file_repository.dart'; import '../../core/repositories/file_repository.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../models/rest/config.dart';
import '../models/rest/history.dart';
import '../models/rest/uploaded_multi_response.dart';
import '../models/rest/uploaded_response.dart';
class FileService { class FileService {
final FileRepository _fileRepository = locator<FileRepository>(); final FileRepository _fileRepository = locator<FileRepository>();
Future getConfig(String url) async { Future<Config> getConfig(String url) async {
return await _fileRepository.getConfig(url); return await _fileRepository.getConfig(url);
} }
Future getHistory() async { FutureOr<History> getHistory() async {
return await _fileRepository.getHistory(); return await _fileRepository.getHistory();
} }
Future deletePaste(String id) async { Future deletePaste(String id) async {
return await _fileRepository.delete(id); return await _fileRepository.postDelete(id);
} }
Future upload(List<File> files, Map<String, String> additionalFiles) async { Future<UploadedResponse> uploadPaste(
return await _fileRepository.upload(files, additionalFiles); List<File>? files, Map<String, String>? additionalFiles) async {
return await _fileRepository.postUpload(files, additionalFiles);
} }
Future createMulti(List<String> ids) async { Future<UploadedMultiResponse> uploadMultiPaste(List<String> ids) async {
return await _fileRepository.createMulti(ids); return await _fileRepository.postCreateMultiPaste(ids);
} }
} }

View file

@ -1,4 +1,4 @@
import 'package:flutter_translate/global.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@ -11,13 +11,16 @@ class LinkService {
final DialogService _dialogService = locator<DialogService>(); final DialogService _dialogService = locator<DialogService>();
Future open(String link) async { Future open(String link) async {
if (await canLaunch(link)) { Uri uri = Uri.parse(link);
await launch(link);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else { } else {
_logger.e('Could not launch link $link'); _logger.e('Could not launch link $link');
_dialogService.showDialog( _dialogService.showDialog(
title: translate('link.dialog.title'), title: translate('link.dialog.title'),
description: translate('link.dialog.description', args: {'link': link})); description:
translate('link.dialog.description', args: {'link': link}));
} }
} }
} }

View file

@ -4,7 +4,7 @@ import 'package:logger/logger.dart';
import '../util/logger.dart'; import '../util/logger.dart';
class NavigationService { class NavigationService {
GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>();
GlobalKey<NavigatorState> get navigationKey => _navigationKey; GlobalKey<NavigatorState> get navigationKey => _navigationKey;
@ -12,16 +12,18 @@ class NavigationService {
void pop() { void pop() {
logger.d('NavigationService: pop'); logger.d('NavigationService: pop');
_navigationKey.currentState.pop(); _navigationKey.currentState!.pop();
} }
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) { Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateTo $routeName'); logger.d('NavigationService: navigateTo $routeName');
return _navigationKey.currentState.pushNamed(routeName, arguments: arguments); return _navigationKey.currentState!
.pushNamed(routeName, arguments: arguments);
} }
Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) { Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) {
logger.d('NavigationService: navigateAndReplaceTo $routeName'); logger.d('NavigationService: navigateAndReplaceTo $routeName');
return _navigationKey.currentState.pushReplacementNamed(routeName, arguments: arguments); return _navigationKey.currentState!
.pushReplacementNamed(routeName, arguments: arguments);
} }
} }

View file

@ -1,115 +1,109 @@
import 'dart:async'; import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter_translate/flutter_translate.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../constants.dart'; import '../../constants.dart';
import '../../core/datamodels/dialog_response.dart';
import '../../core/services/dialog_service.dart';
import '../../core/services/stoppable_service.dart'; import '../../core/services/stoppable_service.dart';
import '../../core/util/logger.dart'; import '../../core/util/logger.dart';
import '../../locator.dart';
import 'storage_service.dart';
class PermissionService extends StoppableService { class PermissionService extends StoppableService {
final Logger _logger = getLogger(); final Logger _logger = getLogger();
final DialogService _dialogService = locator<DialogService>();
final StorageService _storageService = locator<StorageService>();
Timer _serviceCheckTimer; Timer? _serviceCheckTimer;
PermissionStatus _permissionStatus;
bool _permanentlyIgnored = false;
bool _devicePermissionDialogActive = false; bool _devicePermissionDialogActive = false;
bool _ownPermissionDialogActive = false;
PermissionService() { bool _deviceInformationInitialized = false;
_devicePermissionDialogActive = true; bool _useStoragePermission = true;
Permission.storage.request().then((status) { PermissionService();
_permissionStatus = status;
if (PermissionStatus.permanentlyDenied == status) {
_permanentlyIgnored = true;
}
}).whenComplete(() {
_logger.d('Initial device request permission finished');
_devicePermissionDialogActive = false;
});
}
Future checkEnabledAndPermission() async { Future checkEnabledAndPermission() async {
if (_permanentlyIgnored) {
await _storageService.storeStoragePermissionDialogIgnored();
_permanentlyIgnored = false;
_logger.d('Set permanently ignored permission request');
stop();
}
if (_devicePermissionDialogActive) { if (_devicePermissionDialogActive) {
_logger.d('Device permission dialog active, skipping'); _logger.d('Device permission dialog active, skipping');
return; return;
} }
if (_ownPermissionDialogActive) { bool allGranted = false;
_logger.d('Own permission dialog already active, skipping'); bool anyPermanentlyDenied = false;
return;
// Since Android compileSdk >= 33, "storage" is deprecated
// Instead, request access to all of
// - Permission.photos
// - Permission.videos
// - Permission.audio
//
// For iOS and Android < 33, keep using "storage"
if (_useStoragePermission) {
PermissionStatus storagePermission = await Permission.storage.status;
allGranted = PermissionStatus.granted == storagePermission;
anyPermanentlyDenied =
PermissionStatus.permanentlyDenied == storagePermission;
} else {
PermissionStatus photosPermission = await Permission.photos.status;
PermissionStatus videosPermission = await Permission.videos.status;
PermissionStatus audioPermission = await Permission.audio.status;
allGranted = PermissionStatus.granted == photosPermission &&
PermissionStatus.granted == videosPermission &&
PermissionStatus.granted == audioPermission;
anyPermanentlyDenied =
PermissionStatus.permanentlyDenied == photosPermission ||
PermissionStatus.permanentlyDenied == videosPermission ||
PermissionStatus.permanentlyDenied == audioPermission;
} }
var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored(); // show warning to user to manually handle, don't enforce it over and over again
if (anyPermanentlyDenied) {
if (ignoredDialog) { _logger.w(
_logger.d('Permanently ignored permission request, skipping'); "At least one required permission has been denied permanently, stopping service");
stop(); stop();
return; return;
} }
_permissionStatus = await Permission.storage.status; // all good, stop the permission service
if (_permissionStatus != PermissionStatus.granted) { if (allGranted) {
if (_permissionStatus == PermissionStatus.permanentlyDenied) { _logger.d("All permissions have been granted, stopping service");
await _storageService.storeStoragePermissionDialogIgnored(); stop();
return; return;
} }
_ownPermissionDialogActive = true; // not all have been granted, show OS dialog
DialogResponse response = await _dialogService.showConfirmationDialog( _logger.d(
title: translate('permission_service.dialog.title'), "Not all permissions have been granted yet, initializing permission dialog");
description: translate('permission_service.dialog.description'),
buttonTitleAccept: translate('permission_service.dialog.grant'),
buttonTitleDeny: translate('permission_service.dialog.ignore'));
if (!response.confirmed) {
await _storageService.storeStoragePermissionDialogIgnored();
} else {
_devicePermissionDialogActive = true; _devicePermissionDialogActive = true;
Permission.storage.request().then((status) async {
if (PermissionStatus.permanentlyDenied == status) { if (_useStoragePermission) {
await _storageService.storeStoragePermissionDialogIgnored(); await [Permission.storage].request().whenComplete(() {
}
}).whenComplete(() {
_logger.d('Device request permission finished'); _logger.d('Device request permission finished');
_devicePermissionDialogActive = false; _devicePermissionDialogActive = false;
}); });
}
_ownPermissionDialogActive = false;
} else { } else {
await _storageService.storeStoragePermissionDialogIgnored(); await [Permission.photos, Permission.videos, Permission.audio]
.request()
.whenComplete(() {
_logger.d('Device request permission finished');
_devicePermissionDialogActive = false;
});
} }
} }
@override @override
Future start() async { Future start() async {
super.start(); super.start();
await _determineDeviceInfo();
await checkEnabledAndPermission(); await checkEnabledAndPermission();
_serviceCheckTimer = _serviceCheckTimer = Timer.periodic(
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async { const Duration(milliseconds: Constants.mediaPermissionCheckInterval),
(serviceTimer) async {
if (!super.serviceStopped) { if (!super.serviceStopped) {
await checkEnabledAndPermission(); await checkEnabledAndPermission();
} else { } else {
_serviceTimer.cancel(); serviceTimer.cancel();
} }
}); });
_logger.d('PermissionService started'); _logger.d('PermissionService started');
@ -122,9 +116,32 @@ class PermissionService extends StoppableService {
_logger.d('PermissionService stopped'); _logger.d('PermissionService stopped');
} }
Future _determineDeviceInfo() async {
if (_deviceInformationInitialized) {
_logger.d('Device information already initialized, skipping');
return;
}
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
final androidInfo = await deviceInfoPlugin.androidInfo;
if (androidInfo.version.sdkInt >= 33) {
_useStoragePermission = false;
}
}
if (_useStoragePermission) {
_logger.d('Device requires [storage] permission');
} else {
_logger.d('Device requires [photos,videos,audio] permission');
}
_deviceInformationInitialized = true;
}
void _removeServiceCheckTimer() { void _removeServiceCheckTimer() {
if (_serviceCheckTimer != null) { if (_serviceCheckTimer != null) {
_serviceCheckTimer.cancel(); _serviceCheckTimer!.cancel();
_serviceCheckTimer = null; _serviceCheckTimer = null;
_logger.d('Removed service check timer'); _logger.d('Removed service check timer');
} }

View file

@ -3,11 +3,12 @@ import 'dart:async';
import '../enums/refresh_event.dart'; import '../enums/refresh_event.dart';
class RefreshService { class RefreshService {
StreamController<RefreshEvent> refreshHistoryController = StreamController<RefreshEvent>(); StreamController<RefreshEvent> refreshEventController =
StreamController<RefreshEvent>.broadcast();
void addEvent(RefreshEvent event) { void addEvent(RefreshEvent event) {
if (refreshHistoryController.hasListener) { if (refreshEventController.hasListener) {
refreshHistoryController.add(event); refreshEventController.add(event);
} }
} }
} }

View file

@ -4,7 +4,6 @@ import 'package:logger/logger.dart';
import '../../core/services/stoppable_service.dart'; import '../../core/services/stoppable_service.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../models/rest/config.dart';
import '../models/session.dart'; import '../models/session.dart';
import '../services/storage_service.dart'; import '../services/storage_service.dart';
import '../util/logger.dart'; import '../util/logger.dart';
@ -15,13 +14,24 @@ class SessionService extends StoppableService {
final StorageService _storageService = locator<StorageService>(); final StorageService _storageService = locator<StorageService>();
final Api _api = locator<Api>(); final Api _api = locator<Api>();
StreamController<Session> sessionController = StreamController<Session>(); StreamController<Session?> sessionController = StreamController<Session?>();
Future<bool> login(String url, String apiKey, Config config) async { void setApiConfig(String url, String? apiKey) {
_logger.d('Setting API config for session');
_api.setUrl(url); _api.setUrl(url);
_api.addApiKeyAuthorization(apiKey); _api.addApiKeyAuthorization(apiKey);
}
var session = new Session(url: url, apiKey: apiKey, config: config); void unsetApiConfig() {
_logger.d('Removing API config');
_api.removeApiKeyAuthorization();
_api.removeUrl();
}
Future<bool> login(String url, String apiKey) async {
setApiConfig(url, apiKey);
var session = Session(url: url, apiKey: apiKey);
sessionController.add(session); sessionController.add(session);
await _storageService.storeSession(session); await _storageService.storeSession(session);
_logger.d('Session created'); _logger.d('Session created');
@ -29,9 +39,7 @@ class SessionService extends StoppableService {
} }
Future<bool> logout() async { Future<bool> logout() async {
_api.removeApiKeyAuthorization(); unsetApiConfig();
_api.removeUrl();
sessionController.add(null); sessionController.add(null);
_logger.d('Session destroyed'); _logger.d('Session destroyed');
return await _storageService.removeSession(); return await _storageService.removeSession();

View file

@ -5,45 +5,36 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/session.dart'; import '../models/session.dart';
class StorageService { class StorageService {
static const _SESSION_KEY = 'session'; static const _sessionKey = 'session';
static const _LAST_URL_KEY = 'last_url'; static const _lastUrlKey = 'last_url';
static const _STORAGE_PERMISSION_DIALOG_IGNORED = 'storage_permission_ignored';
Future<bool> storeLastUrl(String url) { Future<bool> storeLastUrl(String url) {
return _store(_LAST_URL_KEY, url); return _store(_lastUrlKey, url);
} }
Future<String> retrieveLastUrl() async { Future<String?> retrieveLastUrl() async {
return await _retrieve(_LAST_URL_KEY); return await _retrieve(_lastUrlKey);
} }
Future<bool> hasLastUrl() async { Future<bool> hasLastUrl() async {
return await _exists(_LAST_URL_KEY); return await _exists(_lastUrlKey);
} }
Future<bool> storeSession(Session session) { Future<bool> storeSession(Session session) {
return _store(_SESSION_KEY, json.encode(session)); return _store(_sessionKey, json.encode(session));
} }
Future<Session> retrieveSession() async { Future<Session> retrieveSession() async {
var retrieve = await _retrieve(_SESSION_KEY); var retrieve = await _retrieve(_sessionKey);
return Session.fromJson(json.decode(retrieve)); return Session.fromJson(json.decode(retrieve!));
} }
Future<bool> hasSession() { Future<bool> hasSession() {
return _exists(_SESSION_KEY); return _exists(_sessionKey);
} }
Future<bool> removeSession() { Future<bool> removeSession() {
return _remove(_SESSION_KEY); return _remove(_sessionKey);
}
Future<bool> storeStoragePermissionDialogIgnored() {
return _store(_STORAGE_PERMISSION_DIALOG_IGNORED, true.toString());
}
Future<bool> hasStoragePermissionDialogIgnored() {
return _exists(_STORAGE_PERMISSION_DIALOG_IGNORED);
} }
Future<bool> _exists(String key) async { Future<bool> _exists(String key) async {
@ -56,7 +47,7 @@ class StorageService {
return prefs.remove(key); return prefs.remove(key);
} }
Future<String> _retrieve(String key) async { Future<String?> _retrieve(String key) async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString(key); return prefs.getString(key);
} }

View file

@ -1,12 +1,33 @@
import 'dart:async'; import 'dart:async';
import '../../locator.dart'; import '../../locator.dart';
import '../enums/error_code.dart';
import '../error/service_exception.dart';
import '../models/rest/apikeys_response.dart';
import '../models/rest/create_apikey_response.dart';
import '../repositories/user_repository.dart'; import '../repositories/user_repository.dart';
import 'file_service.dart';
class UserService { class UserService {
final FileService _fileService = locator<FileService>();
final UserRepository _userRepository = locator<UserRepository>(); final UserRepository _userRepository = locator<UserRepository>();
Future createApiKey(String url, String username, String password, String accessLevel, String comment) async { Future<CreateApiKeyResponse> createApiKey(String url, String username,
return await _userRepository.createApiKey(url, username, password, accessLevel, comment); String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey(
url, username, password, accessLevel, comment);
}
Future<ApiKeysResponse> getApiKeys() async {
return await _userRepository.getApiKeys();
}
/// Use 'getHistory' to check currently used API key to require 'apikey' access level
Future<void> checkAccessLevelIsAtLeastApiKey() async {
try {
await _fileService.getHistory();
} on ServiceException catch (e) {
throw ServiceException(code: ErrorCode.invalidApiKey, message: e.message);
}
} }
} }

View file

@ -6,13 +6,14 @@ class FormatterUtil {
/// Format epoch timestamp /// Format epoch timestamp
static String formatEpoch(num millis) { static String formatEpoch(num millis) {
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm(); DateFormat dateFormat = DateFormat().add_yMEd().add_Hm();
return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis)); return dateFormat
.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
} }
static String formatBytes(int bytes, int decimals) { static String formatBytes(int bytes, int decimals) {
if (bytes <= 0) return "0 B"; if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor(); var i = (log(bytes) / log(1024)).floor();
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i]; return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
} }
} }

View file

@ -0,0 +1,6 @@
class PasteUtil {
/// Generate paste link adding a trailing slash
static String generateLink(String url, String id) {
return '$url/$id/';
}
}

View file

@ -1,10 +1,31 @@
import 'package:package_info_plus/package_info_plus.dart';
import '../../core/services/link_service.dart'; import '../../core/services/link_service.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../enums/viewstate.dart';
import 'base_model.dart'; import 'base_model.dart';
class AboutModel extends BaseModel { class AboutModel extends BaseModel {
final LinkService _linkService = locator<LinkService>(); final LinkService _linkService = locator<LinkService>();
PackageInfo packageInfo = PackageInfo(
appName: 'Unknown',
packageName: 'Unknown',
version: 'Unknown',
buildNumber: 'Unknown',
);
void init() async {
await _initPackageInfo();
}
Future<void> _initPackageInfo() async {
setStateView(ViewState.busy);
final PackageInfo info = await PackageInfo.fromPlatform();
packageInfo = info;
setStateView(ViewState.idle);
}
void openLink(String link) { void openLink(String link) {
_linkService.open(link); _linkService.open(link);
} }

View file

@ -5,35 +5,89 @@ import '../../core/util/logger.dart';
import '../enums/viewstate.dart'; import '../enums/viewstate.dart';
class BaseModel extends ChangeNotifier { class BaseModel extends ChangeNotifier {
static const String stateViewKey = 'viewState';
static const String stateMessageKey = 'viewMessage';
final Logger _logger = getLogger(); final Logger _logger = getLogger();
bool _isDisposed = false; bool _isDisposed = false;
ViewState _state = ViewState.Idle; final Map<String, Object?> _stateMap = {
String _stateMessage; stateViewKey: ViewState.idle,
stateMessageKey: null
};
ViewState get state => _state; ViewState? get state => _stateMap[stateViewKey] as ViewState?;
String get stateMessage => _stateMessage; String? get stateMessage => _stateMap[stateMessageKey] as String?;
bool getStateValueAsBoolean(String key) {
if (_stateMap.containsKey(key) && _stateMap[key] is bool) {
return _stateMap[key] as bool;
}
return false;
}
String? getStateValueAsString(String key) {
if (_stateMap.containsKey(key) && _stateMap[key] is String) {
return _stateMap[key] as String;
}
return null;
}
int? getStateValueAsInt(String key) {
if (_stateMap.containsKey(key) && _stateMap[key] is int) {
return _stateMap[key] as int;
}
return null;
}
void setStateBoolValue(String key, bool stateValue) =>
_setStateValue(key, stateValue);
void setStateIntValue(String key, int? stateValue) =>
_setStateValue(key, stateValue);
void setStateStringValue(String key, String? stateValue) =>
_setStateValue(key, stateValue);
void _setStateValue(String key, Object? stateValue) {
if (_stateMap.containsKey(key)) {
_stateMap.update(key, (value) => stateValue);
} else {
_stateMap.putIfAbsent(key, () => stateValue);
}
void setState(ViewState viewState) {
_state = viewState;
if (!_isDisposed) { if (!_isDisposed) {
notifyListeners(); notifyListeners();
_logger.d("Notified state change '${viewState.toString()}'"); _logger
.d("Notified state value update '($key, ${stateValue.toString()})'");
} }
} }
void setStateMessage(String stateMessage) { void removeStateValue(String key) {
_stateMessage = stateMessage; _stateMap.remove(key);
if (!_isDisposed) { if (!_isDisposed) {
notifyListeners(); notifyListeners();
_logger.d("Notified state message change '$stateMessage'"); _logger.d("Notified state removal of '$key'");
} }
} }
void setStateView(ViewState stateView) {
_setStateValue(stateViewKey, stateView);
}
void setStateMessage(String? stateMessage) {
_setStateValue(stateMessageKey, stateMessage);
}
@override @override
void dispose() { void dispose() {
_logger.d("Calling dispose");
super.dispose(); super.dispose();
_isDisposed = true; _isDisposed = true;
} }

View file

@ -28,14 +28,15 @@ class HistoryModel extends BaseModel {
final LinkService _linkService = locator<LinkService>(); final LinkService _linkService = locator<LinkService>();
final DialogService _dialogService = locator<DialogService>(); final DialogService _dialogService = locator<DialogService>();
StreamSubscription _refreshTriggerSubscription; late StreamSubscription _refreshTriggerSubscription;
List<UploadedPaste> pastes = []; List<UploadedPaste> pastes = [];
String errorMessage; String? errorMessage;
void init() { void init() {
this._refreshTriggerSubscription = _refreshService.refreshHistoryController.stream.listen((event) { _refreshTriggerSubscription =
if (event == RefreshEvent.RefreshHistory) { _refreshService.refreshEventController.stream.listen((event) {
if (event == RefreshEvent.refreshHistory) {
_logger.d('History needs a refresh'); _logger.d('History needs a refresh');
getHistory(); getHistory();
} }
@ -43,18 +44,19 @@ class HistoryModel extends BaseModel {
} }
Future getHistory() async { Future getHistory() async {
setState(ViewState.Busy); setStateView(ViewState.busy);
try { try {
pastes.clear(); pastes.clear();
History _history = await _fileService.getHistory(); History history = await _fileService.getHistory();
if (_history.items != null) { if (history.items.isNotEmpty) {
_history.items.forEach((key, value) { history.items.forEach((key, value) {
var millisecondsSinceEpoch = int.parse(value.date) * 1000; var millisecondsSinceEpoch = int.parse(value.date) * 1000;
pastes.add( pastes.add(
UploadedPaste( UploadedPaste(
id: key, id: key,
date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch), date:
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
filename: value.filename, filename: value.filename,
filesize: int.parse(value.filesize), filesize: int.parse(value.filesize),
hash: value.hash, hash: value.hash,
@ -66,8 +68,8 @@ class HistoryModel extends BaseModel {
}); });
} }
if (_history.multipasteItems != null) { if (history.multipasteItems.isNotEmpty) {
_history.multipasteItems.forEach((key, multiPaste) { history.multipasteItems.forEach((key, multiPaste) {
var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000; var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000;
pastes.add(UploadedPaste( pastes.add(UploadedPaste(
id: key, id: key,
@ -77,7 +79,7 @@ class HistoryModel extends BaseModel {
}); });
} }
pastes.sort((a, b) => a.date.compareTo(b.date)); pastes.sort((a, b) => a.date!.compareTo(b.date!));
errorMessage = null; errorMessage = null;
} catch (e) { } catch (e) {
if (e is RestServiceException) { if (e is RestServiceException) {
@ -90,40 +92,43 @@ class HistoryModel extends BaseModel {
e.responseBody is RestError && e.responseBody is RestError &&
e.responseBody.message != null) { e.responseBody.message != null) {
if (e.statusCode == HttpStatus.badRequest) { if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); errorMessage = translate('api.general_rest_error_payload',
args: {'message': e.responseBody.message});
} }
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { } else if (e is ServiceException && e.code == ErrorCode.socketError) {
errorMessage = translate('api.socket_error'); errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { } else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
errorMessage = translate('api.socket_timeout'); errorMessage = translate('api.socket_timeout');
} else { } else {
errorMessage = translate('app.unknown_error'); errorMessage = translate('app.unknown_error');
setState(ViewState.Idle); setStateView(ViewState.idle);
_logger.e('An unknown error occurred', e); _logger.e('An unknown error occurred', error: e);
throw e; rethrow;
} }
} }
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
Future deletePaste(String id) async { Future deletePaste(String id) async {
DialogResponse res = await _dialogService.showConfirmationDialog( DialogResponse res = await _dialogService.showConfirmationDialog(
title: translate('history.delete_dialog.title'), title: translate('history.delete_dialog.title'),
description: translate('history.delete_dialog.description', args: {'id': id}), description:
translate('history.delete_dialog.description', args: {'id': id}),
buttonTitleAccept: translate('history.delete_dialog.accept'), buttonTitleAccept: translate('history.delete_dialog.accept'),
buttonTitleDeny: translate('history.delete_dialog.deny')); buttonTitleDeny: translate('history.delete_dialog.deny'));
if (!res.confirmed) { if (!res.confirmed!) {
return; return;
} }
setState(ViewState.Busy); setStateView(ViewState.busy);
try { try {
await _fileService.deletePaste(id); await _fileService.deletePaste(id);
@ -139,23 +144,24 @@ class HistoryModel extends BaseModel {
e.statusCode != HttpStatus.forbidden && e.statusCode != HttpStatus.forbidden &&
e.responseBody is RestError && e.responseBody is RestError &&
e.responseBody.message != null) { e.responseBody.message != null) {
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); errorMessage = translate('api.general_rest_error_payload',
args: {'message': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { } else if (e is ServiceException && e.code == ErrorCode.socketError) {
errorMessage = translate('api.socket_error'); errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { } else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
errorMessage = translate('api.socket_timeout'); errorMessage = translate('api.socket_timeout');
} else { } else {
errorMessage = translate('app.unknown_error'); errorMessage = translate('app.unknown_error');
setState(ViewState.Idle); setStateView(ViewState.idle);
_logger.e('An unknown error occurred', e); _logger.e('An unknown error occurred', error: e);
throw e; rethrow;
} }
} }
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
void openLink(String link) { void openLink(String link) {

View file

@ -1,12 +1,12 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:validators/sanitizers.dart'; import 'package:validators/sanitizers.dart';
import 'package:validators/validators.dart'; import 'package:validators/validators.dart';
import '../../core/services/file_service.dart';
import '../../core/services/session_service.dart'; import '../../core/services/session_service.dart';
import '../../core/services/storage_service.dart'; import '../../core/services/storage_service.dart';
import '../../locator.dart'; import '../../locator.dart';
@ -14,16 +14,18 @@ import '../enums/error_code.dart';
import '../enums/viewstate.dart'; import '../enums/viewstate.dart';
import '../error/rest_service_exception.dart'; import '../error/rest_service_exception.dart';
import '../error/service_exception.dart'; import '../error/service_exception.dart';
import '../models/rest/config.dart';
import '../models/rest/create_apikey_response.dart'; import '../models/rest/create_apikey_response.dart';
import '../services/user_service.dart'; import '../services/user_service.dart';
import '../util/logger.dart'; import '../util/logger.dart';
import 'base_model.dart'; import 'base_model.dart';
class LoginModel extends BaseModel { class LoginModel extends BaseModel {
TextEditingController _uriController = new TextEditingController(); TextEditingController _uriController = TextEditingController();
final TextEditingController _userNameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController(); final TextEditingController _userNameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _apiKeyController = TextEditingController();
TextEditingController get uriController => _uriController; TextEditingController get uriController => _uriController;
@ -31,104 +33,145 @@ class LoginModel extends BaseModel {
TextEditingController get passwordController => _passwordController; TextEditingController get passwordController => _passwordController;
TextEditingController get apiKeyController => _apiKeyController;
final SessionService _sessionService = locator<SessionService>(); final SessionService _sessionService = locator<SessionService>();
final StorageService _storageService = locator<StorageService>(); final StorageService _storageService = locator<StorageService>();
final UserService _userService = locator<UserService>(); final UserService _userService = locator<UserService>();
final FileService _fileService = locator<FileService>();
final Logger _logger = getLogger(); final Logger _logger = getLogger();
String errorMessage; bool useCredentialsLogin = true;
String? errorMessage;
void toggleLoginMethod() {
setStateView(ViewState.busy);
useCredentialsLogin = !useCredentialsLogin;
setStateView(ViewState.idle);
}
void init() async { void init() async {
bool hasLastUrl = await _storageService.hasLastUrl(); bool hasLastUrl = await _storageService.hasLastUrl();
if (hasLastUrl) { if (hasLastUrl) {
setState(ViewState.Busy); setStateView(ViewState.busy);
var s = await _storageService.retrieveLastUrl(); var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
if (s.isNotEmpty) { if (s.isNotEmpty) {
_uriController = new TextEditingController(text: s); _uriController = TextEditingController(text: s);
} }
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
} }
Future<bool> login(String url, String username, String password) async { Future<bool> login() async {
setState(ViewState.Busy); var url = uriController.text;
var username = userNameController.text;
var password = passwordController.text;
var apiKey = apiKeyController.text;
setStateView(ViewState.busy);
url = trim(url); url = trim(url);
username = trim(username); username = trim(username);
if (url.isEmpty) { if (url.isEmpty) {
errorMessage = translate('login.errors.empty_url'); errorMessage = translate('login.errors.empty_url');
setState(ViewState.Idle); setStateView(ViewState.idle);
return false; return false;
} }
if (!url.contains("https://") && !url.contains("http://")) { if (!url.contains("https://") && !url.contains("http://")) {
errorMessage = translate('login.errors.no_protocol'); errorMessage = translate('login.errors.no_protocol');
setState(ViewState.Idle); setStateView(ViewState.idle);
return false; return false;
} }
bool validUri = Uri.parse(url).isAbsolute; bool validUri = Uri.parse(url).isAbsolute;
if (!validUri || !isURL(url)) { if (!validUri || !isURL(url)) {
errorMessage = translate('login.errors.invalid_url'); errorMessage = translate('login.errors.invalid_url');
setState(ViewState.Idle); setStateView(ViewState.idle);
return false; return false;
} }
if (useCredentialsLogin) {
if (username.isEmpty) { if (username.isEmpty) {
errorMessage = translate('login.errors.empty_username'); errorMessage = translate('login.errors.empty_username');
setState(ViewState.Idle); setStateView(ViewState.idle);
return false; return false;
} }
if (password.isEmpty) { if (password.isEmpty) {
errorMessage = translate('login.errors.empty_password'); errorMessage = translate('login.errors.empty_password');
setState(ViewState.Idle); setStateView(ViewState.idle);
return false; return false;
} }
} else {
if (apiKey.isEmpty) {
errorMessage = translate('login.errors.empty_apikey');
setStateView(ViewState.idle);
return false;
}
}
var success = false; var success = false;
try { try {
Config config = await _fileService.getConfig(url); if (useCredentialsLogin) {
CreateApiKeyResponse apiKeyResponse = CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
await _userService.createApiKey(url, username, password, 'apikey', 'fbmobile'); url,
success = await _sessionService.login(url, apiKeyResponse.data['new_key'], config); username,
password,
'apikey',
'fbmobile-${DateTime.now().millisecondsSinceEpoch}');
var newKey = apiKeyResponse.data['new_key'];
if (newKey != null) {
success = await _sessionService.login(url, newKey);
} else {
throw ServiceException(
code: ErrorCode.invalidApiKey,
message: translate('login.errors.invalid_api_key'));
}
} else {
_sessionService.setApiConfig(url, apiKey);
await _userService.checkAccessLevelIsAtLeastApiKey();
success = await _sessionService.login(url, apiKey);
}
errorMessage = null; errorMessage = null;
} catch (e) { } catch (e) {
if (e is RestServiceException) { if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) { if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials'); errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) { } else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden'); errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) { } else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found'); errorMessage = translate('api.incompatible_error_not_found');
} }
if (e.statusCode == HttpStatus.badRequest) { if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { } else if (e is ServiceException && e.code == ErrorCode.invalidApiKey) {
errorMessage = translate('login.errors.invalid_api_key');
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
errorMessage = translate('api.socket_error'); errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { } else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
errorMessage = translate('api.socket_timeout'); errorMessage = translate('api.socket_timeout');
} else { } else {
errorMessage = translate('app.unknown_error'); errorMessage = translate('app.unknown_error');
_sessionService.logout(); _sessionService.logout();
setState(ViewState.Idle); setStateView(ViewState.idle);
_logger.e('An unknown error occurred', e); _logger.e('An unknown error occurred', error: e);
throw e; rethrow;
} }
if (errorMessage.isNotEmpty) { if (errorMessage!.isNotEmpty) {
_sessionService.logout(); _sessionService.logout();
} }
setState(ViewState.Idle); setStateView(ViewState.idle);
return success; return success;
} }

View file

@ -1,29 +1,106 @@
import 'dart:io';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:logger/logger.dart';
import '../../core/services/session_service.dart'; import '../../core/services/session_service.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../enums/error_code.dart';
import '../error/rest_service_exception.dart';
import '../error/service_exception.dart';
import '../models/rest/config.dart';
import '../services/dialog_service.dart'; import '../services/dialog_service.dart';
import '../services/file_service.dart';
import '../services/link_service.dart'; import '../services/link_service.dart';
import '../util/formatter_util.dart';
import '../util/logger.dart';
import 'base_model.dart'; import 'base_model.dart';
class ProfileModel extends BaseModel { class ProfileModel extends BaseModel {
static const _configurationButtonLoading = 'configurationButtonLoading';
final SessionService _sessionService = locator<SessionService>(); final SessionService _sessionService = locator<SessionService>();
final DialogService _dialogService = locator<DialogService>(); final DialogService _dialogService = locator<DialogService>();
final LinkService _linkService = locator<LinkService>(); final LinkService _linkService = locator<LinkService>();
final FileService _fileService = locator<FileService>();
final Logger _logger = getLogger();
bool get configLoading => getStateValueAsBoolean(_configurationButtonLoading);
String? errorMessage;
Future logout() async { Future logout() async {
var dialogResult = await _dialogService.showConfirmationDialog( var dialogResult = await _dialogService.showConfirmationDialog(
title: translate('logout.title'), description: translate('logout.confirm')); title: translate('logout.title'),
description: translate('logout.confirm'));
if (dialogResult.confirmed) { if (dialogResult.confirmed!) {
await _sessionService.logout(); await _sessionService.logout();
} }
} }
Future revealApiKey(String apiKey) async { Future revealApiKey(String? apiKey) async {
await _dialogService.showDialog( await _dialogService.showDialog(
title: translate('profile.revealed_api_key.title'), title: translate('profile.revealed_api_key.title'),
description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey})); description: translate('profile.revealed_api_key.description',
args: {'apiKey': apiKey}));
}
Future showConfig(String url) async {
setStateBoolValue(_configurationButtonLoading, true);
Config? config;
try {
config = await _fileService.getConfig(url);
errorMessage = null;
} catch (e) {
if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found');
}
if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
} else {
errorMessage = translate('api.general_rest_error');
}
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
errorMessage = translate('api.socket_timeout');
} else {
errorMessage = translate('app.unknown_error');
setStateBoolValue(_configurationButtonLoading, false);
_sessionService.logout();
setStateBoolValue(_configurationButtonLoading, false);
_logger.e('An unknown error occurred', error: e);
rethrow;
}
}
setStateBoolValue(_configurationButtonLoading, false);
if (config != null && errorMessage == null) {
await _dialogService.showDialog(
title: translate('profile.shown_config.title'),
description: translate('profile.shown_config.description', args: {
'uploadMaxSize':
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'maxFilesPerRequest': config.maxFilesPerRequest,
'maxInputVars': config.maxInputVars,
'requestMaxSize':
FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
}));
} else {
await _dialogService.showDialog(
title: translate('profile.shown_config.error.title'),
description: translate('profile.shown_config.error.description',
args: {'message': errorMessage}));
}
} }
void openLink(String link) { void openLink(String link) {

View file

@ -1,3 +1,4 @@
import 'package:fbmobile/core/services/permission_service.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../locator.dart'; import '../../locator.dart';
@ -9,19 +10,21 @@ import 'base_model.dart';
class StartUpViewModel extends BaseModel { class StartUpViewModel extends BaseModel {
final SessionService _sessionService = locator<SessionService>(); final SessionService _sessionService = locator<SessionService>();
final PermissionService _permissionService = locator<PermissionService>();
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService = locator<NavigationService>();
Future handleStartUpLogic() async { Future handleStartUpLogic() async {
setState(ViewState.Busy); setStateView(ViewState.busy);
setStateMessage(translate('startup.init')); setStateMessage(translate('startup.init'));
await Future.delayed(Duration(milliseconds: 150)); await Future.delayed(const Duration(milliseconds: 100));
setStateMessage(translate('startup.start_services')); setStateMessage(translate('startup.start_services'));
await _sessionService.start(); await _sessionService.start();
await Future.delayed(Duration(milliseconds: 150)); await _permissionService.start();
await Future.delayed(const Duration(milliseconds: 100));
_navigationService.navigateAndReplaceTo(HomeView.routeName); _navigationService.navigateAndReplaceTo(HomeView.routeName);
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
} }

View file

@ -4,10 +4,11 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
import 'package:flutter_sharing_intent/model/sharing_file.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../enums/error_code.dart'; import '../enums/error_code.dart';
@ -22,6 +23,7 @@ import '../services/file_service.dart';
import '../services/link_service.dart'; import '../services/link_service.dart';
import '../services/refresh_service.dart'; import '../services/refresh_service.dart';
import '../util/logger.dart'; import '../util/logger.dart';
import '../util/paste_util.dart';
import 'base_model.dart'; import 'base_model.dart';
class UploadModel extends BaseModel { class UploadModel extends BaseModel {
@ -30,88 +32,111 @@ class UploadModel extends BaseModel {
final LinkService _linkService = locator<LinkService>(); final LinkService _linkService = locator<LinkService>();
final RefreshService _refreshService = locator<RefreshService>(); final RefreshService _refreshService = locator<RefreshService>();
TextEditingController _pasteTextController = TextEditingController(); final TextEditingController _pasteTextController = TextEditingController();
StreamSubscription _intentDataStreamSubscription; bool pasteTextTouched = false;
late StreamSubscription _intentDataStreamSubscription;
bool createMulti = false; bool createMulti = false;
String fileName; String? fileName;
List<PlatformFile> paths; List<PlatformFile>? paths;
String _extension; String? _extension;
bool loadingPath = false; bool loadingPath = false;
String errorMessage; String? errorMessage;
TextEditingController get pasteTextController => _pasteTextController; TextEditingController get pasteTextController => _pasteTextController;
void init() { void _parseIntentFiles(List<SharedFile> files) {
// For sharing images coming from outside the app while the app is in the memory if (files.isNotEmpty) {
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) { setStateView(ViewState.busy);
if (value != null && value.length > 0) {
setState(ViewState.Busy); paths = files.map((sharedFile) {
paths = value.map((sharedFile) { _logger.d("Shared file name: ${basename(sharedFile.value ?? '')}");
_logger.d("Shared file path: ${sharedFile.value}");
_logger.d(
"Shared file size: ${File(sharedFile.value ?? '').lengthSync()}");
_logger.d("Shared file type: ${sharedFile.type}");
return PlatformFile.fromMap({ return PlatformFile.fromMap({
'path': sharedFile.path, 'path': sharedFile.value,
'name': basename(sharedFile.path), 'name': basename(sharedFile.value!),
'size': File(sharedFile.path).lengthSync(), 'size': File(sharedFile.value!).lengthSync(),
'bytes': null 'bytes': null
}); });
}).toList(); }).toList();
setState(ViewState.Idle);
setStateView(ViewState.idle);
} }
}
void deleteIntentFile(String path) {
setStateView(ViewState.busy);
_logger.d("Removing path '$path' from $paths");
paths?.removeWhere((element) => element.path == path);
int length = paths!.length;
if (length == 0) {
paths = null;
}
setStateView(ViewState.idle);
}
void init() {
_pasteTextController.addListener(() {
pasteTextTouched = pasteTextController.text.isNotEmpty;
setStateBoolValue("PASTE_TEXT_TOUCHED", pasteTextTouched);
});
// For sharing images coming from outside the app while the app is in the memory
_intentDataStreamSubscription = FlutterSharingIntent.instance
.getMediaStream()
.listen((List<SharedFile> value) {
_logger.d("Retrieved ${value.length} files from intent");
_parseIntentFiles(value);
}, onError: (err) { }, onError: (err) {
setState(ViewState.Busy); _errorIntentHandle(err);
errorMessage = translate('upload.retrieval_intent');
_logger.e('Error while retrieving shared data: $err');
setState(ViewState.Idle);
}); });
// For sharing images coming from outside the app while the app is closed // For sharing images coming from outside the app while the app is closed
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) { FlutterSharingIntent.instance
if (value != null && value.length > 0) { .getInitialSharing()
setState(ViewState.Busy); .then((List<SharedFile> value) {
paths = value.map((sharedFile) { _logger.d("Retrieved ${value.length} files from inactive intent");
return PlatformFile.fromMap({ _parseIntentFiles(value);
'path': sharedFile.path,
'name': basename(sharedFile.path),
'size': File(sharedFile.path).lengthSync(),
'bytes': null
}); });
}).toList();
setState(ViewState.Idle);
} }
});
// For sharing or opening urls/text coming from outside the app while the app is in the memory void _errorIntentHandle(err) {
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) { setStateView(ViewState.busy);
if (value != null && value.isNotEmpty) {
setState(ViewState.Busy);
pasteTextController.text = value;
setState(ViewState.Idle);
}
}, onError: (err) {
setState(ViewState.Busy);
errorMessage = translate('upload.retrieval_intent'); errorMessage = translate('upload.retrieval_intent');
_logger.e('Error while retrieving shared data: $err'); _logger.e('Error while retrieving shared data: $err');
setState(ViewState.Idle); setStateView(ViewState.idle);
}); }
// For sharing or opening urls/text coming from outside the app while the app is closed String? generatePasteLinks(Map<String, bool>? uploads, String url) {
ReceiveSharingIntent.getInitialText().then((String value) { if (uploads != null && uploads.isNotEmpty) {
if (value != null && value.isNotEmpty) { var links = '';
setState(ViewState.Busy);
pasteTextController.text = value; uploads.forEach((id, isMulti) {
setState(ViewState.Idle); if (isMulti && createMulti || !isMulti && !createMulti) {
links += '${PasteUtil.generateLink(url, id)}\n';
} }
}); });
return links;
}
return null;
} }
void toggleCreateMulti() { void toggleCreateMulti() {
setState(ViewState.Busy); setStateView(ViewState.busy);
createMulti = !createMulti; createMulti = !createMulti;
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
void openFileExplorer() async { void openFileExplorer() async {
setState(ViewState.Busy); setStateView(ViewState.busy);
setStateMessage(translate('upload.file_explorer_open')); setStateMessage(translate('upload.file_explorer_open'));
loadingPath = true; loadingPath = true;
@ -121,62 +146,68 @@ class UploadModel extends BaseModel {
allowMultiple: true, allowMultiple: true,
withData: false, withData: false,
withReadStream: true, withReadStream: true,
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null, allowedExtensions: (_extension?.isNotEmpty ?? false)
? _extension?.replaceAll(' ', '').split(',')
: null,
)) ))
?.files; ?.files;
} on PlatformException catch (e) { } on PlatformException catch (e) {
_logger.e('Unsupported operation', e); _logger.e('Unsupported operation', error: e);
} catch (ex) { } catch (ex) {
_logger.e('An unknown error occurred', ex); _logger.e('An unknown error occurred', error: ex);
} }
loadingPath = false; loadingPath = false;
fileName = paths != null ? paths.map((e) => e.name).toString() : '...'; fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
setStateMessage(null); setStateMessage(null);
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
void clearCachedFiles() async { void clearCachedFiles() async {
setState(ViewState.Busy); setStateView(ViewState.busy);
await FilePicker.platform.clearTemporaryFiles(); await FilePicker.platform.clearTemporaryFiles();
paths = null; paths = null;
fileName = null; fileName = null;
errorMessage = null; errorMessage = null;
setState(ViewState.Idle); setStateView(ViewState.idle);
} }
Future<Map<String, bool>> upload() async { Future<Map<String, bool>?> upload() async {
setState(ViewState.Busy); setStateView(ViewState.busy);
setStateMessage(translate('upload.uploading_now')); setStateMessage(translate('upload.uploading_now'));
Map<String, bool> uploadedPasteIds = new Map(); Map<String, bool> uploadedPasteIds = {};
try { try {
List<File> files; List<File>? files;
Map<String, String> additionalFiles; Map<String, String>? additionalFiles;
if (pasteTextController.text != null && pasteTextController.text.isNotEmpty) { if (pasteTextController.text.isNotEmpty) {
additionalFiles = Map.from( additionalFiles = Map.from({
{'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text}); 'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
} pasteTextController.text
if (paths != null && paths.length > 0) {
files = paths.map((e) => new File(e.path)).toList();
}
UploadedResponse response = await _fileService.upload(files, additionalFiles);
response.data.ids.forEach((element) {
uploadedPasteIds.putIfAbsent(element, () => false);
}); });
}
if (paths != null && paths!.isNotEmpty) {
files = paths!.map((e) => File(e.path!)).toList();
}
UploadedResponse response =
await _fileService.uploadPaste(files, additionalFiles);
for (var element in response.data.ids) {
uploadedPasteIds.putIfAbsent(element, () => false);
}
if (createMulti && response.data.ids.length > 1) { if (createMulti && response.data.ids.length > 1) {
UploadedMultiResponse multiResponse = await _fileService.createMulti(response.data.ids); UploadedMultiResponse multiResponse =
await _fileService.uploadMultiPaste(response.data.ids);
uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true); uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true);
} }
clearCachedFiles(); clearCachedFiles();
_pasteTextController.clear(); _pasteTextController.clear();
_refreshService.addEvent(RefreshEvent.RefreshHistory); _refreshService.addEvent(RefreshEvent.refreshHistory);
errorMessage = null; errorMessage = null;
return uploadedPasteIds; return uploadedPasteIds;
} catch (e) { } catch (e) {
@ -190,28 +221,30 @@ class UploadModel extends BaseModel {
e.responseBody is RestError && e.responseBody is RestError &&
e.responseBody.message != null) { e.responseBody.message != null) {
if (e.statusCode == HttpStatus.badRequest) { if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message}); errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
} else { } else {
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message}); errorMessage = translate('api.general_rest_error_payload',
args: {'message': e.responseBody.message});
} }
} else { } else {
errorMessage = translate('api.general_rest_error'); errorMessage = translate('api.general_rest_error');
} }
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) { } else if (e is ServiceException && e.code == ErrorCode.socketError) {
errorMessage = translate('api.socket_error'); errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) { } else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
errorMessage = translate('api.socket_timeout'); errorMessage = translate('api.socket_timeout');
} else { } else {
errorMessage = translate('app.unknown_error'); errorMessage = translate('app.unknown_error');
setStateMessage(null); setStateMessage(null);
setState(ViewState.Idle); setStateView(ViewState.idle);
_logger.e('An unknown error occurred', e); _logger.e('An unknown error occurred', error: e);
throw e; rethrow;
} }
} }
setStateMessage(null); setStateMessage(null);
setState(ViewState.Idle); setStateView(ViewState.idle);
return null; return null;
} }
@ -223,6 +256,8 @@ class UploadModel extends BaseModel {
void dispose() { void dispose() {
_pasteTextController.dispose(); _pasteTextController.dispose();
_intentDataStreamSubscription.cancel(); _intentDataStreamSubscription.cancel();
FlutterSharingIntent.instance.reset();
paths = null;
super.dispose(); super.dispose();
} }
} }

View file

@ -9,10 +9,11 @@ import 'locator.dart';
/// main entry point used to configure log level, locales, ... /// main entry point used to configure log level, locales, ...
void main() async { void main() async {
setupLogger(Level.info); setupLogger(Level.info);
// setupLogger(Level.debug);
setupLocator(); setupLocator();
var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en']); var delegate = await LocalizationDelegate.create(
fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
WidgetsFlutterBinding.ensureInitialized();
runApp(LocalizedApp(delegate, MyApp())); runApp(LocalizedApp(delegate, MyApp()));
} }

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../ui/views/tabbar_container_view.dart'; import '../ui/views/tabbar_container_view.dart';
@ -15,20 +14,21 @@ class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
case StartUpView.routeName: case StartUpView.routeName:
return MaterialPageRoute(builder: (_) => StartUpView()); return MaterialPageRoute(builder: (_) => const StartUpView());
case AboutView.routeName: case AboutView.routeName:
return MaterialPageRoute(builder: (_) => AboutView()); return MaterialPageRoute(builder: (_) => const AboutView());
case HomeView.routeName: case HomeView.routeName:
return MaterialPageRoute(builder: (_) => TabBarContainerView()); return MaterialPageRoute(builder: (_) => const TabBarContainerView());
case LoginView.routeName: case LoginView.routeName:
return MaterialPageRoute(builder: (_) => LoginView()); return MaterialPageRoute(builder: (_) => LoginView());
case ProfileView.routeName: case ProfileView.routeName:
return MaterialPageRoute(builder: (_) => ProfileView()); return MaterialPageRoute(builder: (_) => const ProfileView());
default: default:
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => Scaffold( builder: (_) => Scaffold(
body: Center( body: Center(
child: Text(translate('dev.no_route', args: {'route': settings.name})), child: Text(translate('dev.no_route',
args: {'route': settings.name})),
), ),
)); ));
} }

View file

@ -1,10 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const Color backgroundColor = Colors.white;
/// Colors
const Color primaryBackgroundColor = Colors.white;
const Map<int, Color> colors = { const Map<int, Color> colors = {
50: Color.fromRGBO(63, 69, 75, .1), 50: Color.fromRGBO(63, 69, 75, .1),
100: Color.fromRGBO(63, 69, 75, .2), 100: Color.fromRGBO(63, 69, 75, .2),
@ -19,5 +14,9 @@ const Map<int, Color> colors = {
}; };
const MaterialColor myColor = MaterialColor(0xFF3F454B, colors); const MaterialColor myColor = MaterialColor(0xFF3F454B, colors);
const Color primaryAccentColor = myColor; const Color primaryAccentColor = myColor;
const Color buttonBackgroundColor = primaryAccentColor;
const Color buttonForegroundColor = Colors.white; const Color blueColor = Colors.blue;
const Color whiteColor = Colors.white;
const Color redColor = Colors.red;
const Color orangeColor = Colors.orange;
const Color greenColor = Colors.green;

View file

@ -1,4 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
const headerStyle = TextStyle(fontSize: 35, fontWeight: FontWeight.w900);
const subHeaderStyle = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500); const subHeaderStyle = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500);

View file

@ -1,27 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class UIHelper { class UIHelper {
static const double _VerticalSpaceSmall = 10.0; static const double _verticalSpaceSmall = 10.0;
static const double _VerticalSpaceMedium = 20.0; static const double _verticalSpaceMedium = 20.0;
static const double _VerticalSpaceLarge = 60.0; static const double _verticalSpaceLarge = 60.0;
static const double _HorizontalSpaceSmall = 10.0; static const double _horizontalSpaceSmall = 10.0;
static const double _HorizontalSpaceMedium = 20.0; static const double _horizontalSpaceMedium = 20.0;
static const double HorizontalSpaceLarge = 60.0; static const double _horizontalSpaceLarge = 60.0;
/// Returns a vertical space with height set to [_VerticalSpaceSmall] /// Returns a vertical space with height set to [_verticalSpaceSmall]
static Widget verticalSpaceSmall() { static Widget verticalSpaceSmall() {
return verticalSpace(_VerticalSpaceSmall); return verticalSpace(_verticalSpaceSmall);
} }
/// Returns a vertical space with height set to [_VerticalSpaceMedium] /// Returns a vertical space with height set to [_verticalSpaceMedium]
static Widget verticalSpaceMedium() { static Widget verticalSpaceMedium() {
return verticalSpace(_VerticalSpaceMedium); return verticalSpace(_verticalSpaceMedium);
} }
/// Returns a vertical space with height set to [_VerticalSpaceLarge] /// Returns a vertical space with height set to [_verticalSpaceLarge]
static Widget verticalSpaceLarge() { static Widget verticalSpaceLarge() {
return verticalSpace(_VerticalSpaceLarge); return verticalSpace(_verticalSpaceLarge);
} }
/// Returns a vertical space equal to the [height] supplied /// Returns a vertical space equal to the [height] supplied
@ -29,19 +29,19 @@ class UIHelper {
return Container(height: height); return Container(height: height);
} }
/// Returns a vertical space with height set to [_HorizontalSpaceSmall] /// Returns a vertical space with height set to [_horizontalSpaceSmall]
static Widget horizontalSpaceSmall() { static Widget horizontalSpaceSmall() {
return horizontalSpace(_HorizontalSpaceSmall); return horizontalSpace(_horizontalSpaceSmall);
} }
/// Returns a vertical space with height set to [_HorizontalSpaceMedium] /// Returns a vertical space with height set to [_horizontalSpaceMedium]
static Widget horizontalSpaceMedium() { static Widget horizontalSpaceMedium() {
return horizontalSpace(_HorizontalSpaceMedium); return horizontalSpace(_horizontalSpaceMedium);
} }
/// Returns a vertical space with height set to [HorizontalSpaceLarge] /// Returns a vertical space with height set to [_horizontalSpaceLarge]
static Widget horizontalSpaceLarge() { static Widget horizontalSpaceLarge() {
return horizontalSpace(HorizontalSpaceLarge); return horizontalSpace(_horizontalSpaceLarge);
} }
/// Returns a vertical space equal to the [width] supplied /// Returns a vertical space equal to the [width] supplied

View file

@ -6,13 +6,14 @@ import '../../core/enums/viewstate.dart';
import '../../core/viewmodels/about_model.dart'; import '../../core/viewmodels/about_model.dart';
import '../../ui/shared/text_styles.dart'; import '../../ui/shared/text_styles.dart';
import '../../ui/shared/ui_helpers.dart'; import '../../ui/shared/ui_helpers.dart';
import '../shared/app_colors.dart';
import '../widgets/my_appbar.dart'; import '../widgets/my_appbar.dart';
import 'base_view.dart'; import 'base_view.dart';
class AboutView extends StatelessWidget { class AboutView extends StatelessWidget {
static const routeName = '/about'; static const routeName = '/about';
const AboutView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final logo = Hero( final logo = Hero(
@ -25,31 +26,42 @@ class AboutView extends StatelessWidget {
); );
return BaseView<AboutModel>( return BaseView<AboutModel>(
onModelReady: (model) => model.init(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar( appBar: MyAppBar(
title: Text(translate('titles.about')), title: Text(translate('titles.about')),
enableAbout: false, enableAbout: false,
), ),
backgroundColor: backgroundColor, body: model.state == ViewState.busy
body: model.state == ViewState.Busy ? const Center(child: CircularProgressIndicator())
? Center(child: CircularProgressIndicator())
: Container( : Container(
padding: EdgeInsets.all(0), padding: const EdgeInsets.all(0),
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0), padding: const EdgeInsets.only(
left: 10.0, right: 10.0, bottom: 10, top: 10),
children: <Widget>[ children: <Widget>[
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Center(child: logo), Center(child: logo),
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Center( Center(
child: Text( child: Text(
translate(('about.description')), translate('about.versions', args: {
'appName': model.packageInfo.appName,
'packageName': model.packageInfo.packageName,
'version': model.packageInfo.version,
'buildNumber': model.packageInfo.buildNumber
}),
)), )),
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Center( Center(
child: Text( child: Text(
translate(('about.faq_headline')), translate('about.description'),
)),
UIHelper.verticalSpaceMedium(),
Center(
child: Text(
translate('about.faq_headline'),
style: subHeaderStyle, style: subHeaderStyle,
)), )),
Center( Center(
@ -59,14 +71,14 @@ class AboutView extends StatelessWidget {
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Center( Center(
child: Text( child: Text(
translate(('about.contact_us')), translate('about.contact_us'),
style: subHeaderStyle, style: subHeaderStyle,
)), )),
UIHelper.verticalSpaceSmall(), UIHelper.verticalSpaceSmall(),
Center( Center(
child: Linkify( child: Linkify(
text: translate('about.website'), text: translate('about.website'),
options: LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
onOpen: (link) => model.openLink(link.url), onOpen: (link) => model.openLink(link.url),
), ),
) )

View file

@ -7,10 +7,10 @@ import '../../core/viewmodels/base_model.dart';
import '../../locator.dart'; import '../../locator.dart';
class BaseView<T extends BaseModel> extends StatefulWidget { class BaseView<T extends BaseModel> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget child) builder; final Widget Function(BuildContext context, T model, Widget? child)? builder;
final Function(T) onModelReady; final Function(T)? onModelReady;
BaseView({this.builder, this.onModelReady}); const BaseView({super.key, this.builder, this.onModelReady});
@override @override
_BaseViewState<T> createState() => _BaseViewState<T>(); _BaseViewState<T> createState() => _BaseViewState<T>();
@ -24,14 +24,16 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
@override @override
void initState() { void initState() {
if (widget.onModelReady != null) { if (widget.onModelReady != null) {
widget.onModelReady(model); widget.onModelReady!(model);
} }
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ChangeNotifierProvider<T>(create: (context) => model, child: Consumer<T>(builder: widget.builder)); return ChangeNotifierProvider<T?>(
create: (context) => model,
child: Consumer<T>(builder: widget.builder!));
} }
@override @override

View file

@ -3,11 +3,12 @@ import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:share/share.dart'; import 'package:share_plus/share_plus.dart';
import '../../core/enums/viewstate.dart'; import '../../core/enums/viewstate.dart';
import '../../core/models/session.dart'; import '../../core/models/session.dart';
import '../../core/util/formatter_util.dart'; import '../../core/util/formatter_util.dart';
import '../../core/util/paste_util.dart';
import '../../core/viewmodels/history_model.dart'; import '../../core/viewmodels/history_model.dart';
import '../../ui/widgets/centered_error_row.dart'; import '../../ui/widgets/centered_error_row.dart';
import '../shared/app_colors.dart'; import '../shared/app_colors.dart';
@ -17,10 +18,10 @@ import 'base_view.dart';
class HistoryView extends StatelessWidget { class HistoryView extends StatelessWidget {
static const routeName = '/history'; static const routeName = '/history';
const HistoryView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var url = Provider.of<Session>(context).url;
return BaseView<HistoryModel>( return BaseView<HistoryModel>(
onModelReady: (model) { onModelReady: (model) {
model.init(); model.init();
@ -28,34 +29,42 @@ class HistoryView extends StatelessWidget {
}, },
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.history'))), appBar: MyAppBar(title: Text(translate('titles.history'))),
backgroundColor: backgroundColor, body: _render(model, context)),
body: model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: (model.errorMessage == null
? Container(
padding: EdgeInsets.all(0),
child: RefreshIndicator(onRefresh: () => model.getHistory(), child: _render(model, url, context)))
: Container(
padding: EdgeInsets.all(25),
child: CenteredErrorRow(
model.errorMessage,
retryCallback: () => model.getHistory(),
)))),
); );
} }
Widget _render(HistoryModel model, String url, BuildContext context) { Widget _render(HistoryModel model, BuildContext context) {
var url = Provider.of<Session>(context).url;
return model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
: (model.errorMessage == null
? Container(
padding: const EdgeInsets.all(0),
child: RefreshIndicator(
onRefresh: () async => await model.getHistory(),
child: _renderItems(model, url, context)))
: Container(
padding: const EdgeInsets.all(25),
child: CenteredErrorRow(
model.errorMessage,
retryCallback: () => model.getHistory(),
)));
}
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
List<Widget> cards = []; List<Widget> cards = [];
if (model.pastes.length > 0) { if (model.pastes.isNotEmpty) {
model.pastes.reversed.forEach((paste) { for (var paste in model.pastes.reversed) {
List<Widget> widgets = []; List<Widget> widgets = [];
var fullPasteUrl = '$url/${paste.id}'; var fullPasteUrl = PasteUtil.generateLink(url, paste.id);
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl); var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
var dateWidget = ListTile( var dateWidget = ListTile(
title: Text(FormatterUtil.formatEpoch(paste.date.millisecondsSinceEpoch)), title: Text(
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
subtitle: Text(translate('history.date')), subtitle: Text(translate('history.date')),
); );
@ -67,39 +76,42 @@ class HistoryView extends StatelessWidget {
var copyWidget = ListTile( var copyWidget = ListTile(
title: Text(translate('history.copy_link.description')), title: Text(translate('history.copy_link.description')),
trailing: IconButton( trailing: IconButton(
icon: Icon(Icons.copy, color: Colors.blue, textDirection: TextDirection.ltr), icon: const Icon(Icons.copy,
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () { onPressed: () {
FlutterClipboard.copy(fullPasteUrl).then((value) { FlutterClipboard.copy(fullPasteUrl).then((value) {
final snackBar = SnackBar( final snackBar = SnackBar(
action: SnackBarAction( action: SnackBarAction(
label: translate('history.copy_link.dismiss'), label: translate('history.copy_link.dismiss'),
textColor: Colors.blue, textColor: blueColor,
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).hideCurrentSnackBar();
}, },
), ),
content: Text(translate('history.copy_link.copied')), content: Text(translate('history.copy_link.copied')),
duration: Duration(seconds: 10), duration: const Duration(seconds: 10),
); );
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar); ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
}); });
})); }));
var deleteWidget = ListTile( var deleteWidget = ListTile(
title: Text(translate('history.delete')), title: Text(translate('history.delete')),
trailing: IconButton( trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red), icon: const Icon(Icons.delete, color: redColor),
onPressed: () { onPressed: () {
return model.deletePaste(paste.id); model.deletePaste(paste.id);
})); }));
if (!paste.isMulti) { if (!paste.isMulti!) {
var titleWidget = ListTile( var titleWidget = ListTile(
title: Text(paste.filename ?? paste.id), title: Text(paste.filename ?? paste.id),
subtitle: Text(translate('history.filename')), subtitle: Text(translate('history.filename')),
); );
var fileSizeWidget = ListTile( var fileSizeWidget = ListTile(
title: Text(FormatterUtil.formatBytes(paste.filesize, 2)), title: Text(FormatterUtil.formatBytes(paste.filesize as int, 2)),
subtitle: Text(translate('history.filesize')), subtitle: Text(translate('history.filesize')),
); );
var idWidget = ListTile( var idWidget = ListTile(
@ -107,7 +119,7 @@ class HistoryView extends StatelessWidget {
subtitle: Text(translate('history.id')), subtitle: Text(translate('history.id')),
); );
var mimeTypeWidget = ListTile( var mimeTypeWidget = ListTile(
title: Text(paste.mimetype), title: Text(paste.mimetype!),
subtitle: Text(translate('history.mimetype')), subtitle: Text(translate('history.mimetype')),
); );
@ -116,13 +128,13 @@ class HistoryView extends StatelessWidget {
widgets.add(fileSizeWidget); widgets.add(fileSizeWidget);
widgets.add(mimeTypeWidget); widgets.add(mimeTypeWidget);
} else { } else {
paste.items.forEach((element) { for (var element in paste.items!) {
widgets.add(ListTile( widgets.add(ListTile(
title: Text(element), title: Text(element!),
subtitle: Text(translate('history.multipaste_element')), subtitle: Text(translate('history.multipaste_element')),
trailing: _renderOpenInBrowser(model, '$url/$element'), trailing: _renderOpenInBrowser(model, '$url/$element'),
)); ));
}); }
} }
widgets.add(dateWidget); widgets.add(dateWidget);
@ -131,18 +143,18 @@ class HistoryView extends StatelessWidget {
widgets.add(deleteWidget); widgets.add(deleteWidget);
var expandable = ExpandableTheme( var expandable = ExpandableTheme(
data: ExpandableThemeData( data: const ExpandableThemeData(
iconPlacement: ExpandablePanelIconPlacement.right, iconPlacement: ExpandablePanelIconPlacement.right,
headerAlignment: ExpandablePanelHeaderAlignment.center, headerAlignment: ExpandablePanelHeaderAlignment.center,
hasIcon: true, hasIcon: true,
iconColor: Colors.blue, iconColor: blueColor,
tapHeaderToExpand: true), tapHeaderToExpand: true),
child: ExpandablePanel( child: ExpandablePanel(
collapsed: Container(),
header: InkWell( header: InkWell(
onLongPress: () => model.deletePaste(paste.id),
child: Text( child: Text(
paste.id, paste.id,
style: TextStyle(color: Colors.blue), style: const TextStyle(color: blueColor),
textAlign: TextAlign.left, textAlign: TextAlign.left,
)), )),
expanded: Column( expanded: Column(
@ -158,15 +170,17 @@ class HistoryView extends StatelessWidget {
trailing: Wrap(children: [ trailing: Wrap(children: [
openInBrowserButton, openInBrowserButton,
IconButton( IconButton(
icon: Icon(Icons.share, color: Colors.blue, textDirection: TextDirection.ltr), icon: const Icon(Icons.share,
onPressed: () { color: blueColor, textDirection: TextDirection.ltr),
return Share.share(fullPasteUrl); onPressed: () async {
await Share.share(fullPasteUrl);
}) })
]), ]),
subtitle: Text(!paste.isMulti ? paste.filename : '', style: TextStyle(fontStyle: FontStyle.italic)), subtitle: Text(!paste.isMulti! ? paste.filename! : '',
style: const TextStyle(fontStyle: FontStyle.italic)),
), ),
)); ));
}); }
} else { } else {
cards.add(Card( cards.add(Card(
child: ListTile( child: ListTile(
@ -177,14 +191,15 @@ class HistoryView extends StatelessWidget {
return ListView( return ListView(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
physics: const AlwaysScrollableScrollPhysics(),
children: cards, children: cards,
physics: AlwaysScrollableScrollPhysics(),
); );
} }
Widget _renderOpenInBrowser(HistoryModel model, String url) { Widget _renderOpenInBrowser(HistoryModel model, String url) {
return IconButton( return IconButton(
icon: Icon(Icons.open_in_new, color: Colors.blue, textDirection: TextDirection.ltr), icon: const Icon(Icons.open_in_new,
color: blueColor, textDirection: TextDirection.ltr),
onPressed: () { onPressed: () {
return model.openLink(url); return model.openLink(url);
}); });

View file

@ -3,20 +3,22 @@ import 'package:flutter_translate/flutter_translate.dart';
import '../../core/enums/viewstate.dart'; import '../../core/enums/viewstate.dart';
import '../../core/viewmodels/home_model.dart'; import '../../core/viewmodels/home_model.dart';
import '../shared/app_colors.dart';
import '../widgets/my_appbar.dart'; import '../widgets/my_appbar.dart';
import 'base_view.dart'; import 'base_view.dart';
class HomeView extends StatelessWidget { class HomeView extends StatelessWidget {
static const routeName = '/home'; static const routeName = '/home';
const HomeView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BaseView<HomeModel>( return BaseView<HomeModel>(
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('app.title'))), appBar: MyAppBar(title: Text(translate('app.title'))),
backgroundColor: backgroundColor, body: model.state == ViewState.busy
body: model.state == ViewState.Busy ? Center(child: CircularProgressIndicator()) : Container()), ? const Center(child: CircularProgressIndicator())
: Container()),
); );
} }
} }

View file

@ -6,31 +6,29 @@ import '../../core/services/dialog_service.dart';
import '../../core/services/navigation_service.dart'; import '../../core/services/navigation_service.dart';
import '../../core/viewmodels/login_model.dart'; import '../../core/viewmodels/login_model.dart';
import '../../locator.dart'; import '../../locator.dart';
import '../../ui/shared/text_styles.dart';
import '../../ui/views/home_view.dart'; import '../../ui/views/home_view.dart';
import '../../ui/widgets/my_appbar.dart'; import '../../ui/widgets/my_appbar.dart';
import '../shared/app_colors.dart'; import '../shared/app_colors.dart';
import '../widgets/login_header.dart'; import '../shared/ui_helpers.dart';
import '../widgets/login_header_apikey.dart';
import '../widgets/login_header_credentials.dart';
import 'base_view.dart'; import 'base_view.dart';
class LoginView extends StatefulWidget { class LoginView extends StatelessWidget {
static const routeName = '/login'; static const routeName = '/login';
@override
_LoginViewState createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService = locator<NavigationService>();
final DialogService _dialogService = locator<DialogService>(); final DialogService _dialogService = locator<DialogService>();
LoginView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final logo = Hero( final logo = Hero(
tag: 'hero', tag: 'hero',
child: CircleAvatar( child: CircleAvatar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
radius: 96.0, radius: 36.0,
child: Image.asset('assets/logo_caption.png'), child: Image.asset('assets/logo_caption.png'),
), ),
); );
@ -39,45 +37,62 @@ class _LoginViewState extends State<LoginView> {
onModelReady: (model) => model.init(), onModelReady: (model) => model.init(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.login'))), appBar: MyAppBar(title: Text(translate('titles.login'))),
backgroundColor: backgroundColor, body: model.state == ViewState.busy
body: model.state == ViewState.Busy ? const Center(child: CircularProgressIndicator())
? Center(child: CircularProgressIndicator())
: ListView( : ListView(
shrinkWrap: true, shrinkWrap: true,
padding: EdgeInsets.only(left: 24.0, right: 24.0), padding: const EdgeInsets.only(left: 10.0, right: 10.0),
children: <Widget>[ children: <Widget>[
UIHelper.verticalSpaceMedium(),
Center(child: logo), Center(child: logo),
UIHelper.verticalSpaceMedium(),
Center( Center(
child: Wrap( child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: <Widget>[ children: <Widget>[
Text(
translate('login.help'),
style: subHeaderStyle,
),
InkWell( InkWell(
child: Icon(Icons.help, color: buttonBackgroundColor), child: const Icon(Icons.help),
onTap: () { onTap: () {
_dialogService.showDialog( _dialogService.showDialog(
title: translate('login.compatibility_dialog.title'), title: translate(
description: translate('login.compatibility_dialog.body')); 'login.compatibility_dialog.title'),
description: translate(
'login.compatibility_dialog.body'));
},
),
InkWell(
child: Icon(
model.useCredentialsLogin
? Icons.person_outline
: Icons.vpn_key,
color: blueColor),
onTap: () {
model.toggleLoginMethod();
}, },
) )
])), ])),
LoginHeaders( UIHelper.verticalSpaceMedium(),
model.useCredentialsLogin
? LoginCredentialsHeaders(
validationMessage: model.errorMessage, validationMessage: model.errorMessage,
uriController: model.uriController, uriController: model.uriController,
usernameController: model.userNameController, usernameController: model.userNameController,
passwordController: model.passwordController, passwordController: model.passwordController,
), )
ElevatedButton( : LoginApiKeyHeaders(
child: Text(translate('login.button'), style: TextStyle(color: buttonForegroundColor)), validationMessage: model.errorMessage,
uriController: model.uriController,
apiKeyController: model.apiKeyController),
UIHelper.verticalSpaceMedium(),
ElevatedButton.icon(
icon: const Icon(Icons.login, color: blueColor),
label: Text(translate('login.button')),
onPressed: () async { onPressed: () async {
var loginSuccess = await model.login( var loginSuccess = await model.login();
model.uriController.text, model.userNameController.text, model.passwordController.text);
if (loginSuccess) { if (loginSuccess) {
_navigationService.navigateAndReplaceTo(HomeView.routeName); _navigationService
.navigateAndReplaceTo(HomeView.routeName);
} }
}, },
) )

View file

@ -0,0 +1,73 @@
import 'package:fbmobile/core/util/logger.dart';
import 'package:fbmobile/ui/views/profile_view.dart';
import 'package:fbmobile/ui/views/upload_view.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';
import '../shared/app_colors.dart';
import 'history_view.dart';
class AuthenticatedNavBarView extends StatefulWidget {
const AuthenticatedNavBarView({super.key});
@override
AuthenticatedNavBarState createState() => AuthenticatedNavBarState();
}
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView>
with SingleTickerProviderStateMixin {
final Logger _logger = getLogger();
int _currentTabIndex = 0;
void updateIndex(int targetIndex) {
setState(() {
_currentTabIndex = targetIndex;
_logger.d("Changing current tab index to '$targetIndex'");
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
key: UniqueKey(),
onDestinationSelected: (int index) {
updateIndex(index);
},
selectedIndex: _currentTabIndex,
labelBehavior: NavigationDestinationLabelBehavior.alwaysHide,
destinations: const <Widget>[
NavigationDestination(
icon: Icon(Icons.upload_outlined),
label: 'Upload',
),
NavigationDestination(
icon: Icon(Icons.history_outlined),
label: 'History',
),
NavigationDestination(
icon: Icon(Icons.person_outlined),
label: 'Profile',
),
],
),
body: <Widget>[
Container(
color: myColor,
alignment: Alignment.center,
child: const UploadView(),
),
Container(
color: myColor,
alignment: Alignment.center,
child: const HistoryView(),
),
Container(
color: myColor,
alignment: Alignment.center,
child: const ProfileView(),
),
][_currentTabIndex],
);
}
}

View file

@ -5,7 +5,6 @@ import 'package:provider/provider.dart';
import '../../core/enums/viewstate.dart'; import '../../core/enums/viewstate.dart';
import '../../core/models/session.dart'; import '../../core/models/session.dart';
import '../../core/util/formatter_util.dart';
import '../../core/viewmodels/profile_model.dart'; import '../../core/viewmodels/profile_model.dart';
import '../shared/app_colors.dart'; import '../shared/app_colors.dart';
import '../shared/text_styles.dart'; import '../shared/text_styles.dart';
@ -16,69 +15,87 @@ import 'base_view.dart';
class ProfileView extends StatelessWidget { class ProfileView extends StatelessWidget {
static const routeName = '/profile'; static const routeName = '/profile';
const ProfileView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var url = Provider.of<Session>(context).url;
var apiKey = Provider.of<Session>(context).apiKey;
var config = Provider.of<Session>(context).config;
return BaseView<ProfileModel>( return BaseView<ProfileModel>(
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.profile'))), appBar: MyAppBar(title: Text(translate('titles.profile'))),
floatingActionButton: FloatingActionButton( body: _render(model, context)));
heroTag: "logoutButton", }
child: Icon(Icons.exit_to_app),
backgroundColor: primaryAccentColor, Widget _render(ProfileModel model, BuildContext context) {
onPressed: () { var url = Provider.of<Session>(context).url;
model.logout(); var apiKey = Provider.of<Session>(context).apiKey;
},
), return model.state == ViewState.busy
backgroundColor: backgroundColor, ? const Center(child: CircularProgressIndicator())
body: model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: ListView( : ListView(
children: <Widget>[ children: <Widget>[
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Padding( Padding(
padding: const EdgeInsets.only(left: 25.0), padding: const EdgeInsets.only(left: 25.0),
child: Center(
child: Text( child: Text(
translate('profile.welcome'), translate('profile.instance'),
style: headerStyle, style: subHeaderStyle,
), ))),
),
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Padding( Padding(
padding: const EdgeInsets.only(left: 25.0), padding: const EdgeInsets.only(left: 25.0),
child: Center(
child: Linkify( child: Linkify(
onOpen: (link) => model.openLink(link.url), onOpen: (link) => model.openLink(link.url),
text: translate('profile.connection', args: {'url': url}), text: translate('profile.connection', args: {'url': url}),
options: LinkifyOptions(humanize: false), options: const LinkifyOptions(humanize: false),
)), ))),
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Padding( Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0), padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: Icon(Icons.remove_red_eye, color: Colors.blue), icon: model.configLoading
label: Text( ? Container(
translate('profile.reveal_api_key'), width: 24,
style: TextStyle(color: buttonForegroundColor), height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: blueColor,
strokeWidth: 3,
), ),
onPressed: () { )
return model.revealApiKey(apiKey); : const Icon(Icons.settings, color: blueColor),
label: Text(
model.configLoading
? translate('profile.show_config_loading')
: translate('profile.show_config'),
),
onPressed: () async {
await model.showConfig(url);
})), })),
UIHelper.verticalSpaceMedium(), UIHelper.verticalSpaceMedium(),
Padding( Padding(
padding: const EdgeInsets.only(left: 25.0), padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: Text( child: ElevatedButton.icon(
translate('profile.config', args: { icon: const Icon(Icons.lock, color: orangeColor),
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize, 2), label: Text(
'maxFilesPerRequest': config.maxFilesPerRequest, translate('profile.reveal_api_key'),
'maxInputVars': config.maxInputVars, ),
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize, 2) onPressed: () {
}), model.revealApiKey(apiKey);
)), })),
UIHelper.verticalSpaceMedium(),
Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.exit_to_app, color: redColor),
label: Text(
translate('profile.logout'),
),
onPressed: () async {
await model.logout();
})),
], ],
)),
); );
} }
} }

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider_architecture/provider_architecture.dart'; import 'package:stacked/stacked.dart';
import '../../core/enums/viewstate.dart'; import '../../core/enums/viewstate.dart';
import '../../core/viewmodels/startup_model.dart'; import '../../core/viewmodels/startup_model.dart';
@ -7,21 +7,24 @@ import '../../core/viewmodels/startup_model.dart';
class StartUpView extends StatelessWidget { class StartUpView extends StatelessWidget {
static const routeName = '/'; static const routeName = '/';
const StartUpView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ViewModelProvider<StartUpViewModel>.withConsumer( return ViewModelBuilder<StartUpViewModel>.reactive(
viewModelBuilder: () => StartUpViewModel(), viewModelBuilder: () => StartUpViewModel(),
onModelReady: (model) => model.handleStartUpLogic(), onViewModelReady: (model) => model.handleStartUpLogic(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
backgroundColor: Colors.white, body: model.state == ViewState.busy
body: model.state == ViewState.Busy
? Center( ? Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
CircularProgressIndicator(), const CircularProgressIndicator(),
(model.stateMessage.isNotEmpty ? Text(model.stateMessage) : Container()) (model.stateMessage!.isNotEmpty
? Text(model.stateMessage!)
: Container())
])) ]))
: Container())); : Container()));
} }

View file

@ -1,60 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../shared/app_colors.dart';
import 'login_view.dart';
class AnonymousTabBarView extends StatefulWidget {
@override
AnonymousTabBarState createState() => AnonymousTabBarState();
}
class AnonymousTabBarState extends State<AnonymousTabBarView> with SingleTickerProviderStateMixin {
TabController _tabController;
int _currentTabIndex = 0;
List<Widget> _realPages = [LoginView()];
List<Widget> _tabPages = [LoginView()];
List<bool> _hasInit = [true];
List<Widget> _tabsButton = [Tab(icon: Icon(Icons.person_outline, color: Colors.blue), text: translate('tabs.login'))];
@override
void initState() {
super.initState();
_tabController = TabController(length: _realPages.length, vsync: this)
..addListener(() {
int selectedIndex = _tabController.index;
if (_currentTabIndex != selectedIndex) {
if (!_hasInit[selectedIndex]) {
_tabPages[selectedIndex] = _realPages[selectedIndex];
_hasInit[selectedIndex] = true;
}
setState(() => _currentTabIndex = selectedIndex);
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _currentTabIndex, children: _tabPages),
bottomNavigationBar: BottomAppBar(
child: TabBar(
labelColor: primaryAccentColor,
indicatorColor: Colors.blue,
indicatorWeight: 3.0,
tabs: _tabsButton,
controller: _tabController,
),
),
);
}
}

View file

@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../shared/app_colors.dart';
import 'history_view.dart';
import 'profile_view.dart';
import 'upload_view.dart';
class AuthenticatedTabBarView extends StatefulWidget {
@override
AuthenticatedTabBarState createState() => AuthenticatedTabBarState();
}
class AuthenticatedTabBarState extends State<AuthenticatedTabBarView> with SingleTickerProviderStateMixin {
TabController _tabController;
int _currentTabIndex = 0;
List<Widget> _realPages = [UploadView(), HistoryView(), ProfileView()];
List<Widget> _tabPages = [
UploadView(),
Container(),
Container(),
];
List<bool> _hasInit = [true, false, false];
@override
void initState() {
super.initState();
_tabController = TabController(length: _realPages.length, vsync: this)
..addListener(() {
int selectedIndex = _tabController.index;
if (_currentTabIndex != selectedIndex) {
if (!_hasInit[selectedIndex]) {
_tabPages[selectedIndex] = _realPages[selectedIndex];
_hasInit[selectedIndex] = true;
}
setState(() => _currentTabIndex = selectedIndex);
}
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double yourWidth = width / 3;
double yourHeight = 55;
List<Widget> _tabsButton = [
Container(
width: yourWidth,
height: yourHeight,
alignment: Alignment.center,
child: Tab(
icon: Icon(
Icons.upload_file,
color: _currentTabIndex == 0 ? Colors.blue : primaryAccentColor,
),
text: translate('tabs.upload'))),
Container(
width: yourWidth,
height: yourHeight,
alignment: Alignment.center,
child: Tab(
icon: Icon(
Icons.history,
color: _currentTabIndex == 1 ? Colors.blue : primaryAccentColor,
),
text: translate('tabs.history'))),
Container(
width: yourWidth,
height: yourHeight,
alignment: Alignment.center,
child: Tab(
icon: Icon(
Icons.person,
color: _currentTabIndex == 2 ? Colors.blue : primaryAccentColor,
),
text: translate('tabs.profile'))),
];
return Scaffold(
body: IndexedStack(index: _currentTabIndex, children: _tabPages),
bottomNavigationBar: BottomAppBar(
child: TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelColor: primaryAccentColor,
indicatorColor: Colors.blue,
indicatorWeight: 3.0,
labelPadding: EdgeInsets.all(0),
tabs: _tabsButton,
isScrollable: true,
controller: _tabController,
)),
);
}
}

View file

@ -1,20 +1,23 @@
import 'package:fbmobile/ui/views/login_view.dart';
import 'package:fbmobile/ui/views/navbar_authenticated.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../core/models/session.dart'; import '../../core/models/session.dart';
import 'tabbar_anonymous.dart';
import 'tabbar_authenticated.dart';
class TabBarContainerView extends StatelessWidget { class TabBarContainerView extends StatelessWidget {
const TabBarContainerView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Session currentSession = Provider.of<Session>(context); Session? currentSession = Provider.of<Session?>(context);
bool isAuthenticated = currentSession != null && currentSession.apiKey.isNotEmpty; bool isAuthenticated =
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
if (isAuthenticated) { if (isAuthenticated) {
return AuthenticatedTabBarView(); return const AuthenticatedNavBarView();
} }
return AnonymousTabBarView(); return LoginView();
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:fbmobile/core/util/formatter_util.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -14,24 +15,34 @@ import 'base_view.dart';
class UploadView extends StatelessWidget { class UploadView extends StatelessWidget {
static const routeName = '/upload'; static const routeName = '/upload';
const UploadView({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var url = Provider.of<Session>(context).url;
return BaseView<UploadModel>( return BaseView<UploadModel>(
onModelReady: (model) => model.init(), onModelReady: (model) => model.init(),
builder: (context, model, child) => Scaffold( builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.upload'))), appBar: MyAppBar(title: Text(translate('titles.upload'))),
backgroundColor: backgroundColor, body: _render(model, context)));
body: model.state == ViewState.Busy }
bool _isUploadButtonEnabled(UploadModel model) {
return model.pasteTextTouched ||
(model.paths != null && model.paths!.isNotEmpty);
}
Widget _render(UploadModel model, BuildContext context) {
var url = Provider.of<Session>(context).url;
return model.state == ViewState.busy
? Center( ? Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
CircularProgressIndicator(), const CircularProgressIndicator(),
(model.stateMessage != null && model.stateMessage.isNotEmpty (model.stateMessage != null && model.stateMessage!.isNotEmpty
? Text(model.stateMessage) ? Text(model.stateMessage!)
: Container()) : Container())
])) ]))
: ListView(children: <Widget>[ : ListView(children: <Widget>[
@ -46,17 +57,19 @@ class UploadView extends StatelessWidget {
minLines: 1, minLines: 1,
maxLines: 7, maxLines: 7,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: Icon( prefixIcon: const Icon(
Icons.text_snippet, Icons.text_snippet,
color: buttonBackgroundColor,
), ),
suffixIcon: IconButton( suffixIcon: IconButton(
onPressed: () => model.pasteTextController.clear(), onPressed: () =>
icon: Icon(Icons.clear), model.pasteTextController.clear(),
icon: const Icon(Icons.clear),
), ),
hintText: translate('upload.text_to_be_pasted'), hintText: translate('upload.text_to_be_pasted'),
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0), contentPadding: const EdgeInsets.fromLTRB(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)), 20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0)),
), ),
controller: model.pasteTextController)), controller: model.pasteTextController)),
Padding( Padding(
@ -72,20 +85,21 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
icon: Icon(Icons.file_copy_sharp, color: Colors.blue), icon: const Icon(Icons.file_copy_sharp,
color: blueColor),
onPressed: () => model.openFileExplorer(), onPressed: () => model.openFileExplorer(),
label: Text( label: Text(
translate('upload.open_file_explorer'), translate('upload.open_file_explorer'),
style: TextStyle(color: buttonForegroundColor),
)), )),
ElevatedButton.icon( ElevatedButton.icon(
icon: Icon(Icons.cancel, color: Colors.orange), icon: const Icon(Icons.cancel,
onPressed: model.paths != null && model.paths.length > 0 color: orangeColor),
onPressed: model.paths != null &&
model.paths!.isNotEmpty
? () => model.clearCachedFiles() ? () => model.clearCachedFiles()
: null, : null,
label: Text( label: Text(
translate('upload.clear_temporary_files'), translate('upload.clear_temporary_files'),
style: TextStyle(color: buttonForegroundColor),
)), )),
], ],
)), )),
@ -108,81 +122,116 @@ class UploadView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () async { onPressed: !_isUploadButtonEnabled(model)
Map<String, bool> items = await model.upload(); ? null
: () async {
Map<String, bool>? items =
await model.upload();
String? clipboardContent = model
.generatePasteLinks(items, url);
if (items != null) { if (clipboardContent != null &&
var clipboardContent = ''; clipboardContent.isNotEmpty) {
items.forEach((id, isMulti) { FlutterClipboard.copy(
if (isMulti && model.createMulti || !isMulti && !model.createMulti) { clipboardContent)
clipboardContent += '$url/$id\n'; .then((value) {
}
});
FlutterClipboard.copy(clipboardContent).then((value) {
final snackBar = SnackBar( final snackBar = SnackBar(
action: SnackBarAction( action: SnackBarAction(
label: translate('upload.dismiss'), label: translate(
textColor: Colors.blue, 'upload.dismiss'),
textColor: blueColor,
onPressed: () { onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(
context)
.hideCurrentSnackBar();
}, },
), ),
content: Text(translate('upload.uploaded')), content: Text(translate(
duration: Duration(seconds: 10), 'upload.uploaded')),
duration:
const Duration(seconds: 10),
); );
ScaffoldMessenger.of(context).showSnackBar(snackBar); if (context.mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(snackBar);
}
}); });
} }
}, },
icon: Icon(Icons.upload_rounded, color: Colors.green), icon: const Icon(Icons.upload_rounded,
color: greenColor),
label: Text( label: Text(
translate('upload.upload'), translate('upload.upload'),
style: TextStyle(color: buttonForegroundColor),
)), )),
])), ])),
model.errorMessage != null && model.errorMessage.isNotEmpty model.errorMessage != null && model.errorMessage!.isNotEmpty
? (Padding( ? (Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), padding:
const EdgeInsets.only(top: 10.0, bottom: 10.0),
child: CenteredErrorRow(model.errorMessage))) child: CenteredErrorRow(model.errorMessage)))
: Container(), : Container(),
Builder( Builder(
builder: (BuildContext context) => model.loadingPath builder: (BuildContext context) => model.loadingPath
? Padding( ? const Padding(
padding: const EdgeInsets.only(bottom: 10.0), padding: EdgeInsets.only(bottom: 10.0),
child: const CircularProgressIndicator(), child: CircularProgressIndicator(),
) )
: model.paths != null : model.paths != null
? Container( ? Container(
padding: const EdgeInsets.only(bottom: 30.0), padding: const EdgeInsets.only(bottom: 20.0),
height: MediaQuery.of(context).size.height * 0.50, height:
MediaQuery.of(context).size.height * 0.50,
child: ListView.separated( child: ListView.separated(
itemCount: itemCount: model.paths != null &&
model.paths != null && model.paths.isNotEmpty ? model.paths.length : 1, model.paths!.isNotEmpty
itemBuilder: (BuildContext context, int index) { ? model.paths!.length
final bool isMultiPath = model.paths != null && model.paths.isNotEmpty; : 1,
itemBuilder:
(BuildContext context, int index) {
final bool isMultiPath =
model.paths != null &&
model.paths!.isNotEmpty;
final String name = (isMultiPath final String name = (isMultiPath
? model.paths.map((e) => e.name).toList()[index] ? model.paths!
.map((e) => e.name)
.toList()[index]
: model.fileName ?? '...'); : model.fileName ?? '...');
final path = model.paths.length > 0 final size = model.paths!.isNotEmpty
? model.paths.map((e) => e.path).toList()[index].toString() ? model.paths!
.map((e) => e.size)
.toList()[index]
.toString()
: '';
final path = model.paths!.isNotEmpty
? model.paths!
.map((e) => e.path)
.toList()[index]
.toString()
: ''; : '';
return Card( return Card(
child: ListTile( child: ListTile(
trailing: IconButton(
icon: const Icon(Icons.clear,
color: orangeColor),
onPressed: () {
model.deleteIntentFile(path);
}),
title: Text( title: Text(
name, "$name (${FormatterUtil.formatBytes(int.parse(size), 2)})",
), ),
subtitle: Text(path), subtitle: Text(path),
)); ));
}, },
separatorBuilder: (BuildContext context, int index) => const Divider(), separatorBuilder:
(BuildContext context, int index) =>
const Divider(),
), ),
) )
: Container(), : Container(),
), ),
], ],
)) ))
]))); ]);
} }
} }

View file

@ -5,15 +5,14 @@ import '../../locator.dart';
import '../../ui/views/about_view.dart'; import '../../ui/views/about_view.dart';
class AboutIconButton extends StatelessWidget { class AboutIconButton extends StatelessWidget {
AboutIconButton(); AboutIconButton({super.key});
final NavigationService _navigationService = locator<NavigationService>(); final NavigationService _navigationService = locator<NavigationService>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IconButton( return IconButton(
icon: Icon(Icons.help), icon: const Icon(Icons.help),
color: Colors.white,
onPressed: () { onPressed: () {
_navigationService.navigateTo(AboutView.routeName); _navigationService.navigateTo(AboutView.routeName);
}); });

View file

@ -1,13 +1,12 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../shared/app_colors.dart'; import '../shared/app_colors.dart';
class CenteredErrorRow extends StatelessWidget { class CenteredErrorRow extends StatelessWidget {
final Function retryCallback; final Function? retryCallback;
final String message; final String? message;
CenteredErrorRow(this.message, {this.retryCallback}); const CenteredErrorRow(this.message, {super.key, this.retryCallback});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -21,7 +20,10 @@ class CenteredErrorRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Expanded(child: Center(child: Text(message, style: TextStyle(color: Colors.red)))), Expanded(
child: Center(
child: Text(message!,
style: const TextStyle(color: redColor)))),
], ],
), ),
(retryCallback != null (retryCallback != null
@ -31,10 +33,10 @@ class CenteredErrorRow extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Center( Center(
child: IconButton( child: IconButton(
icon: Icon(Icons.refresh), icon: const Icon(Icons.refresh),
color: primaryAccentColor, color: primaryAccentColor,
onPressed: () { onPressed: () {
retryCallback(); retryCallback!();
}, },
)) ))
]) ])

View file

@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
class LoginHeaders extends StatelessWidget {
final TextEditingController uriController;
final TextEditingController usernameController;
final TextEditingController passwordController;
final String validationMessage;
LoginHeaders(
{@required this.uriController,
@required this.usernameController,
@required this.passwordController,
this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
this.validationMessage != null ? Text(validationMessage, style: TextStyle(color: Colors.red)) : Container(),
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person),
keyboardType: TextInputType.name),
LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key),
obscureText: true),
]);
}
}
class LoginTextField extends StatelessWidget {
final TextEditingController controller;
final String placeHolder;
final TextInputType keyboardType;
final bool obscureText;
final Widget prefixIcon;
LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
{this.keyboardType = TextInputType.text, this.obscureText = false});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10.0),
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
height: 50.0,
alignment: Alignment.centerLeft,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(10.0)),
child: TextFormField(
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () => controller.clear(),
icon: Icon(Icons.clear),
),
prefixIcon: prefixIcon,
hintText: placeHolder,
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
),
controller: controller),
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../shared/app_colors.dart';
import 'login_text_field.dart';
class LoginApiKeyHeaders extends StatelessWidget {
final TextEditingController uriController;
final TextEditingController apiKeyController;
final String? validationMessage;
const LoginApiKeyHeaders(
{super.key,
required this.uriController,
required this.apiKeyController,
this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
validationMessage != null
? Text(validationMessage!, style: const TextStyle(color: redColor))
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(
apiKeyController,
translate('login.apikey_placeholder'),
const Icon(Icons.vpn_key),
obscureText: true,
),
]);
}
}

View file

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../shared/app_colors.dart';
import 'login_text_field.dart';
class LoginCredentialsHeaders extends StatelessWidget {
final TextEditingController uriController;
final TextEditingController usernameController;
final TextEditingController passwordController;
final String? validationMessage;
const LoginCredentialsHeaders(
{super.key,
required this.uriController,
required this.usernameController,
required this.passwordController,
this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
validationMessage != null
? Text(validationMessage!, style: const TextStyle(color: redColor))
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(usernameController,
translate('login.username_placeholder'), const Icon(Icons.person),
keyboardType: TextInputType.name),
LoginTextField(passwordController,
translate('login.password_placeholder'), const Icon(Icons.vpn_key),
obscureText: true),
]);
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class LoginTextField extends StatelessWidget {
final TextEditingController controller;
final String placeHolder;
final TextInputType keyboardType;
final bool obscureText;
final Widget prefixIcon;
const LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
{super.key,
this.keyboardType = TextInputType.text,
this.obscureText = false});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
height: 50.0,
alignment: Alignment.centerLeft,
child: TextFormField(
keyboardType: keyboardType,
obscureText: obscureText,
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () => controller.clear(),
icon: const Icon(Icons.clear),
),
prefixIcon: prefixIcon,
hintText: placeHolder,
contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
border:
OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
),
controller: controller),
);
}
}

View file

@ -6,13 +6,18 @@ class MyAppBar extends AppBar {
static final List<Widget> aboutEnabledWidgets = [AboutIconButton()]; static final List<Widget> aboutEnabledWidgets = [AboutIconButton()];
static final List<Widget> aboutDisabledWidgets = []; static final List<Widget> aboutDisabledWidgets = [];
MyAppBar({Key key, Widget title, List<Widget> actionWidgets, bool enableAbout = true}) MyAppBar(
: super(key: key, title: Row(children: <Widget>[title]), actions: _renderIconButtons(actionWidgets, enableAbout)); {super.key,
required Widget title,
List<Widget>? actionWidgets,
bool enableAbout = true})
: super(
title: Row(children: <Widget>[title]),
actions: _renderIconButtons(actionWidgets, enableAbout));
static List<Widget> _renderIconButtons(List<Widget> actionWidgets, bool aboutEnabled) { static List<Widget> _renderIconButtons(
if (actionWidgets == null) { List<Widget>? actionWidgets, bool aboutEnabled) {
actionWidgets = []; actionWidgets ??= [];
}
List<Widget> widgets = [...actionWidgets]; List<Widget> widgets = [...actionWidgets];

Some files were not shown because too many files have changed in this diff Show more