Compare commits
189 commits
Author | SHA1 | Date | |
---|---|---|---|
ae2a6b8b32 | |||
dce3c916a2 | |||
58141fe646 | |||
dd5abc48f9 | |||
bf1f1e3295 | |||
e7463ffaef | |||
ae454abb3a | |||
34bcd2ee92 | |||
d2485a91cd | |||
e8c8808c0e | |||
b544003941 | |||
5668c15547 | |||
7f145f6960 | |||
d0dd798008 | |||
f16ee08079 | |||
a4a3c2e5dc | |||
a5e9597207 | |||
f460c68339 | |||
c7af6a874d | |||
e0973735d8 | |||
dc715e38d1 | |||
eb1d949af8 | |||
4a715aba48 | |||
c48c16d63d | |||
57db10e912 | |||
a4defff982 | |||
3ae2c37813 | |||
d9b33feac9 | |||
941b7c81ad | |||
5bc02638f7 | |||
477e557d31 | |||
c917e478a6 | |||
8b96610fa6 | |||
df40808bd7 | |||
dfc5772b93 | |||
3d6ae1a9ef | |||
e49dd86347 | |||
4cd6165d9b | |||
e661171fd2 | |||
c99df89cb0 | |||
03d61dcdd0 | |||
f2d1083620 | |||
b5694ae9c6 | |||
9dd84b20a9 | |||
8bb310fb40 | |||
47987b8dad | |||
3d68dcefab | |||
b51131de8f | |||
7e871bb4b1 | |||
88b9d81bc2 | |||
7c7befe071 | |||
2461a02f9d | |||
15104d7e40 | |||
733f09baab | |||
1758eeed18 | |||
c9764e780b | |||
3754d71dec | |||
8904d49eb2 | |||
0e7bc55bef | |||
089d07846d | |||
3f64f86acf | |||
7c91d8b6ca | |||
df1b038782 | |||
c9ef125abb | |||
5a8750b877 | |||
8c10e42c0f | |||
96d8cbea99 | |||
685b140135 | |||
f51e781a9f | |||
16c1a597fc | |||
71ac72a6cc | |||
0bc3ae1da8 | |||
fc5b8783d3 | |||
5492fed0b5 | |||
30810868b2 | |||
a889a6fbee | |||
36e5bcab1f | |||
3b06c2b80b | |||
e4cfa81672 | |||
2837054383 | |||
4deb9b92cb | |||
b0edda0432 | |||
886632957d | |||
264609dff3 | |||
d87796f47b | |||
42f40d52d2 | |||
292660a970 | |||
51e2c476c7 | |||
44cc8bac28 | |||
9b571fb1a4 | |||
d63ef288eb | |||
549b218922 | |||
f61c5a4a1b | |||
a06bbf6751 | |||
e980f1ae53 | |||
66a5f4e9af | |||
39b1465efc | |||
22d096ae43 | |||
08d935284d | |||
6b87873cc7 | |||
41987331b0 | |||
f19d23a757 | |||
67076779a3 | |||
6d302c4392 | |||
53cd587a42 | |||
769dd7e513 | |||
a3ee146e25 | |||
5e9947969e | |||
689c8b4408 | |||
7cee219b19 | |||
803cd11cc4 | |||
a99662af3c | |||
3d11fde4e9 | |||
43cb6472f2 | |||
67dafa615f | |||
fc0fe31b70 | |||
0e4dcbde22 | |||
65a0a294a6 | |||
548d02362b | |||
c50e9ec7ba | |||
e5518b4ade | |||
0b093302ec | |||
4114feb56e | |||
d8c9027509 | |||
e1075f5725 | |||
b49f10c875 | |||
494420cbd3 | |||
aedc7f2bbb | |||
9ac363070b | |||
580099856f | |||
d56d717de0 | |||
3cf96ad5ab | |||
26d0e0851b | |||
c772d950e2 | |||
0630035e45 | |||
3fb46fc766 | |||
53c65ce54f | |||
9a1db51f23 | |||
664b734544 | |||
0e69e904b4 | |||
09b608a168 | |||
091e344831 | |||
0f9e8b398f | |||
88d85257a7 | |||
06566d3d6e | |||
35bdd06f72 | |||
e59164e803 | |||
eec38b455c | |||
ee6230399a | |||
31d91bafc1 | |||
82f9ab5535 | |||
66493cff91 | |||
210c3e7aa4 | |||
d6b645112e | |||
713d8f57be | |||
bc777d4826 | |||
ad9a3a15f5 | |||
dbb4929939 | |||
adb55fd73b | |||
004133f0c0 | |||
67903d1331 | |||
0a0fbe039b | |||
6ef8a9daa0 | |||
47b45a9084 | |||
06dab63e05 | |||
9c331cf6d3 | |||
40451dbabc | |||
6bb4c177e2 | |||
81eb1af2bd | |||
3e7bc379f1 | |||
e9929bd3c3 | |||
9386661adb | |||
46a6df9f80 | |||
577f9cd42c | |||
d8d89167a9 | |||
6d75672add | |||
fc88d5c22f | |||
a2ee915463 | |||
6e5fa0eaa8 | |||
8c3bf06b87 | |||
5e9ac38ca0 | |||
a5dab51765 | |||
a65c7d9253 | |||
bac39aebdf | |||
b55e932204 | |||
f9a2bb0df7 | |||
a0789e6883 | |||
2e7778926a | |||
c2c8ffc5c5 |
89 changed files with 1713 additions and 895 deletions
14
.drone.yml
14
.drone.yml
|
@ -1,14 +0,0 @@
|
|||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: cirrusci/flutter:3.3.9
|
||||
commands:
|
||||
- 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
|
21
.forgejo/workflows/build.yaml
Normal file
21
.forgejo/workflows/build.yaml
Normal 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
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.lock
|
||||
!Gemfile.lock
|
||||
!pubspec.lock
|
||||
*.log
|
||||
*.pyc
|
||||
|
|
29
CHANGELOG.md
29
CHANGELOG.md
|
@ -1,5 +1,34 @@
|
|||
# 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
|
||||
|
|
|
@ -404,6 +404,14 @@ where to find the applicable terms.
|
|||
form of a separately written license, or stated as exceptions;
|
||||
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.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
|
|
31
README.md
31
README.md
|
@ -2,7 +2,8 @@
|
|||
|
||||
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.
|
||||
|
@ -94,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.
|
||||
|
||||
#### 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
|
||||
`Fastlane` file which lanes are present. Run a lane via `fastlane <platform> <lane>`, e.g. use the
|
||||
|
@ -104,13 +105,37 @@ following to build for Android `fastlane android build`.
|
|||
|
||||
##### 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
|
||||
|
||||
For iOS you need to execute `fastlane ios build` before uploading to testflight with
|
||||
`fastlane ios beta`.
|
||||
|
||||
Probably do the same Ruby/fastlane setup as mentioned under the _Android_ section.
|
||||
|
||||
### Release manually (not recommended)
|
||||
|
||||
See the following links on how to setup:
|
||||
|
|
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal 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
2
android/.gitignore
vendored
|
@ -5,3 +5,5 @@ gradle-wrapper.jar
|
|||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.bundle
|
||||
vendor/
|
||||
|
|
222
android/Gemfile.lock
Normal file
222
android/Gemfile.lock
Normal 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
|
|
@ -1,3 +1,9 @@
|
|||
plugins {
|
||||
id "com.android.application"
|
||||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
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')
|
||||
if (flutterVersionCode == null) {
|
||||
flutterVersionCode = '1'
|
||||
|
@ -21,9 +22,6 @@ if (flutterVersionName == null) {
|
|||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
|
@ -31,7 +29,9 @@ if (keystorePropertiesFile.exists()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdkVersion 34
|
||||
|
||||
namespace "de.varakh.fbmobile"
|
||||
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
|
@ -39,8 +39,8 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "de.varakh.fbmobile"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 33
|
||||
minSdkVersion 30
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<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>
|
||||
|
|
|
@ -21,36 +21,31 @@
|
|||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</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" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -70,7 +65,10 @@
|
|||
</application>
|
||||
|
||||
<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>
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<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>
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
@ -24,6 +13,6 @@ subprojects {
|
|||
project.evaluationDependsOn(':app')
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
tasks.register("clean", Delete) {
|
||||
delete rootProject.buildDir
|
||||
}
|
||||
|
|
|
@ -11,6 +11,11 @@ platform :android do
|
|||
sh("#{ENV['PWD']}/fastlane/buildAndroidProduction.sh")
|
||||
end
|
||||
|
||||
desc "Build Production fdroid"
|
||||
lane :build_production_fdroid do
|
||||
sh("#{ENV['PWD']}/fastlane/buildAndroidProductionFdroid.sh")
|
||||
end
|
||||
|
||||
desc "Build"
|
||||
lane :build do
|
||||
sh("#{ENV['PWD']}/fastlane/buildAndroid.sh")
|
||||
|
|
|
@ -31,6 +31,14 @@ Build Debug
|
|||
|
||||
Build Production
|
||||
|
||||
### android build_production_fdroid
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android build_production_fdroid
|
||||
```
|
||||
|
||||
Build Production fdroid
|
||||
|
||||
### android build
|
||||
|
||||
```sh
|
||||
|
|
9
android/fastlane/buildAndroidProductionFdroid.sh
Executable file
9
android/fastlane/buildAndroidProductionFdroid.sh
Executable 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;
|
|
@ -1,4 +1,6 @@
|
|||
agpVersion=8.7.2
|
||||
kotlinVersion=1.7.10
|
||||
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#Fri Jun 23 08:50:38 CEST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
|
|
|
@ -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()
|
||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
||||
if (pluginsFile.exists()) {
|
||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins.each { name, path ->
|
||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
||||
include ":$name"
|
||||
project(":$name").projectDir = pluginDirectory
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "${agpVersion}" apply false
|
||||
id "org.jetbrains.kotlin.android" version "${kotlinVersion}" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
|
|
@ -18,12 +18,6 @@
|
|||
"about": "About",
|
||||
"upload": "Upload"
|
||||
},
|
||||
"tabs": {
|
||||
"login": "Login",
|
||||
"history": "History",
|
||||
"profile": "Profile",
|
||||
"upload": "New"
|
||||
},
|
||||
"upload": {
|
||||
"and_or": "and/or",
|
||||
"open_file_explorer": "Select file(s)...",
|
||||
|
@ -45,7 +39,6 @@
|
|||
"start_services": "Starting services..."
|
||||
},
|
||||
"login": {
|
||||
"help": "Login",
|
||||
"compatibility_dialog": {
|
||||
"title": "How to login?",
|
||||
"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."
|
||||
|
@ -133,14 +126,6 @@
|
|||
"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": {
|
||||
"confirm": "OK",
|
||||
"cancel": "Cancel"
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
</dict>
|
||||
<dict/>
|
||||
</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>
|
||||
<string>Allow to select photos and upload them via the app</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
|
29
lib/app.dart
29
lib/app.dart
|
@ -18,10 +18,12 @@ import 'ui/shared/app_colors.dart';
|
|||
import 'ui/views/startup_view.dart';
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
static final _defaultLightColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.light);
|
||||
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.dark);
|
||||
static final _defaultLightColorScheme = ColorScheme.fromSwatch(
|
||||
primarySwatch: myColor, brightness: Brightness.light);
|
||||
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
|
||||
primarySwatch: myColor, brightness: Brightness.dark);
|
||||
|
||||
MyApp() {
|
||||
MyApp({super.key}) {
|
||||
initializeDateFormatting('en');
|
||||
}
|
||||
|
||||
|
@ -33,26 +35,33 @@ class MyApp extends StatelessWidget {
|
|||
state: LocalizationProvider.of(context).state,
|
||||
child: StreamProvider<RefreshEvent?>(
|
||||
initialData: null,
|
||||
create: (context) => locator<RefreshService>().refreshEventController.stream,
|
||||
create: (context) =>
|
||||
locator<RefreshService>().refreshEventController.stream,
|
||||
child: StreamProvider<Session?>(
|
||||
initialData: Session.initial(),
|
||||
create: (context) => locator<SessionService>().sessionController.stream,
|
||||
child: LifeCycleManager(child: DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
|
||||
create: (context) =>
|
||||
locator<SessionService>().sessionController.stream,
|
||||
child: LifeCycleManager(child: DynamicColorBuilder(
|
||||
builder: (lightColorScheme, darkColorScheme) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: translate('app.title'),
|
||||
builder: (context, child) => Navigator(
|
||||
key: locator<DialogService>().dialogNavigationKey,
|
||||
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
||||
onGenerateRoute: (settings) => MaterialPageRoute(
|
||||
builder: (context) => DialogManager(child: child)),
|
||||
),
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: lightColorScheme ?? _defaultLightColorScheme),
|
||||
darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
|
||||
colorScheme:
|
||||
lightColorScheme ?? _defaultLightColorScheme),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
|
||||
onGenerateRoute: AppRouter.generateRoute,
|
||||
navigatorKey: locator<NavigationService>().navigationKey,
|
||||
home: StartUpView(),
|
||||
home: const StartUpView(),
|
||||
supportedLocales: localizationDelegate.supportedLocales,
|
||||
locale: localizationDelegate.currentLocale,
|
||||
);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
/// Enums for error codes
|
||||
enum ErrorCode {
|
||||
/// A generic error
|
||||
GENERAL_ERROR,
|
||||
generalError,
|
||||
|
||||
/// Errors related to connections
|
||||
SOCKET_ERROR,
|
||||
SOCKET_TIMEOUT,
|
||||
socketError,
|
||||
socketTimeout,
|
||||
|
||||
/// A REST error (response code wasn't 200 or 204)
|
||||
REST_ERROR,
|
||||
restError,
|
||||
|
||||
/// Custom errors
|
||||
INVALID_API_KEY
|
||||
invalidApiKey
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
enum RefreshEvent { RefreshHistory }
|
||||
enum RefreshEvent { refreshHistory }
|
||||
|
|
|
@ -1 +1 @@
|
|||
enum ViewState { Idle, Busy }
|
||||
enum ViewState { idle, busy }
|
||||
|
|
|
@ -5,9 +5,10 @@ class RestServiceException extends ServiceException {
|
|||
final int statusCode;
|
||||
final dynamic responseBody;
|
||||
|
||||
RestServiceException(this.statusCode, {this.responseBody, String? message})
|
||||
: super(code: ErrorCode.REST_ERROR, message: message);
|
||||
RestServiceException(this.statusCode, {this.responseBody, super.message = null})
|
||||
: super(code: ErrorCode.restError);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$code $statusCode $message";
|
||||
}
|
||||
|
|
|
@ -4,8 +4,9 @@ class ServiceException implements Exception {
|
|||
final ErrorCode code;
|
||||
final String? message;
|
||||
|
||||
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''});
|
||||
ServiceException({this.code = ErrorCode.generalError, this.message = ''});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$code: $message";
|
||||
}
|
||||
|
|
|
@ -8,13 +8,14 @@ import '../services/dialog_service.dart';
|
|||
class DialogManager extends StatefulWidget {
|
||||
final Widget? child;
|
||||
|
||||
DialogManager({Key? key, this.child}) : super(key: key);
|
||||
const DialogManager({super.key, this.child});
|
||||
|
||||
@override
|
||||
_DialogManagerState createState() => _DialogManagerState();
|
||||
}
|
||||
|
||||
class _DialogManagerState extends State<DialogManager> {
|
||||
DialogService _dialogService = locator<DialogService>();
|
||||
final DialogService _dialogService = locator<DialogService>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -30,7 +31,8 @@ class _DialogManagerState extends State<DialogManager> {
|
|||
void _showDialog(DialogRequest request) {
|
||||
List<Widget> actions = <Widget>[];
|
||||
|
||||
if (request.buttonTitleDeny != null && request.buttonTitleDeny!.isNotEmpty) {
|
||||
if (request.buttonTitleDeny != null &&
|
||||
request.buttonTitleDeny!.isNotEmpty) {
|
||||
Widget denyBtn = TextButton(
|
||||
child: Text(request.buttonTitleDeny!),
|
||||
onPressed: () {
|
||||
|
|
|
@ -11,15 +11,20 @@ import '../util/logger.dart';
|
|||
class LifeCycleManager extends StatefulWidget {
|
||||
final Widget? child;
|
||||
|
||||
LifeCycleManager({Key? key, this.child}) : super(key: key);
|
||||
const LifeCycleManager({super.key, this.child});
|
||||
|
||||
@override
|
||||
_LifeCycleManagerState createState() => _LifeCycleManagerState();
|
||||
}
|
||||
|
||||
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
|
||||
class _LifeCycleManagerState extends State<LifeCycleManager>
|
||||
with WidgetsBindingObserver {
|
||||
final Logger logger = getLogger();
|
||||
|
||||
List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()];
|
||||
List<StoppableService> servicesToManage = [
|
||||
locator<SessionService>(),
|
||||
locator<PermissionService>()
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -43,12 +48,12 @@ class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBinding
|
|||
logger.d('LifeCycle event ${state.toString()}');
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
servicesToManage.forEach((service) {
|
||||
for (var service in servicesToManage) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
service.start();
|
||||
} else {
|
||||
service.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ class ApiKey {
|
|||
|
||||
final String? comment;
|
||||
|
||||
ApiKey({required this.key, required this.created, required this.accessLevel, this.comment});
|
||||
ApiKey(
|
||||
{required this.key,
|
||||
required this.created,
|
||||
required this.accessLevel,
|
||||
this.comment});
|
||||
|
||||
// JSON Init
|
||||
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);
|
||||
|
|
|
@ -12,7 +12,8 @@ class ApiKeys {
|
|||
ApiKeys({required this.apikeys});
|
||||
|
||||
// JSON Init
|
||||
factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
|
||||
factory ApiKeys.fromJson(Map<String, dynamic> json) =>
|
||||
_$ApiKeysFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class ApiKeysResponse {
|
|||
ApiKeysResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
|
||||
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$ApiKeysResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class ConfigResponse {
|
|||
ConfigResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json);
|
||||
factory ConfigResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConfigResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);
|
||||
|
|
|
@ -13,7 +13,8 @@ class CreateApiKeyResponse {
|
|||
CreateApiKeyResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
|
||||
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$CreateApiKeyResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);
|
||||
|
|
|
@ -19,7 +19,8 @@ class History {
|
|||
History({required this.items, required this.multipasteItems, this.totalSize});
|
||||
|
||||
// JSON Init
|
||||
factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);
|
||||
factory History.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$HistoryToJson(this);
|
||||
|
|
|
@ -23,7 +23,8 @@ class HistoryItem {
|
|||
this.thumbnail});
|
||||
|
||||
// JSON Init
|
||||
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json);
|
||||
factory HistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryItemFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$HistoryItemToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class HistoryMultipasteItem {
|
|||
HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
|
||||
|
||||
// JSON Init
|
||||
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json);
|
||||
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryMultipasteItemFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);
|
||||
|
|
|
@ -9,7 +9,8 @@ class HistoryMultipasteItemEntry {
|
|||
HistoryMultipasteItemEntry({required this.id});
|
||||
|
||||
// JSON Init
|
||||
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemEntryFromJson(json);
|
||||
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryMultipasteItemEntryFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class HistoryResponse {
|
|||
HistoryResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json);
|
||||
factory HistoryResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class RestError {
|
|||
required this.errorId,
|
||||
}); // JSON Init
|
||||
|
||||
factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json);
|
||||
factory RestError.fromJson(Map<String, dynamic> json) =>
|
||||
_$RestErrorFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$RestErrorToJson(this);
|
||||
|
|
|
@ -13,7 +13,8 @@ class Uploaded {
|
|||
Uploaded({required this.ids, required this.urls});
|
||||
|
||||
// JSON Init
|
||||
factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json);
|
||||
factory Uploaded.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadedFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$UploadedToJson(this);
|
||||
|
|
|
@ -13,7 +13,8 @@ class UploadedMulti {
|
|||
UploadedMulti({required this.url, required this.urlId});
|
||||
|
||||
// JSON Init
|
||||
factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json);
|
||||
factory UploadedMulti.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadedMultiFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class UploadedMultiResponse {
|
|||
UploadedMultiResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json);
|
||||
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadedMultiResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);
|
||||
|
|
|
@ -15,7 +15,8 @@ class UploadedResponse {
|
|||
UploadedResponse({required this.status, required this.data});
|
||||
|
||||
// JSON Init
|
||||
factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json);
|
||||
factory UploadedResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadedResponseFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);
|
||||
|
|
|
@ -13,7 +13,8 @@ class Session {
|
|||
: url = '',
|
||||
apiKey = '';
|
||||
|
||||
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
|
||||
factory Session.fromJson(Map<String, dynamic> json) =>
|
||||
_$SessionFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$SessionToJson(this);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ class UploadedPaste {
|
|||
this.items});
|
||||
|
||||
// JSON Init
|
||||
factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json);
|
||||
factory UploadedPaste.fromJson(Map<String, dynamic> json) =>
|
||||
_$UploadedPasteFromJson(json);
|
||||
|
||||
// JSON Export
|
||||
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);
|
||||
|
|
|
@ -11,7 +11,7 @@ import '../models/rest/uploaded_response.dart';
|
|||
import '../services/api.dart';
|
||||
|
||||
class FileRepository {
|
||||
Api _api = locator<Api>();
|
||||
final Api _api = locator<Api>();
|
||||
|
||||
Future<History> getHistory() async {
|
||||
var response = await _api.post('/file/history');
|
||||
|
@ -33,19 +33,23 @@ class FileRepository {
|
|||
return response;
|
||||
}
|
||||
|
||||
Future<UploadedResponse> postUpload(List<File>? files, Map<String, String>? additionalFiles) async {
|
||||
var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles);
|
||||
Future<UploadedResponse> postUpload(
|
||||
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));
|
||||
}
|
||||
|
||||
Future<UploadedMultiResponse> postCreateMultiPaste(List<String> ids) async {
|
||||
Map<String, String> multiPasteIds = Map();
|
||||
Map<String, String> multiPasteIds = {};
|
||||
|
||||
ids.forEach((element) {
|
||||
multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element);
|
||||
});
|
||||
for (var element in ids) {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@ import '../models/rest/create_apikey_response.dart';
|
|||
import '../services/api.dart';
|
||||
|
||||
class UserRepository {
|
||||
Api _api = locator<Api>();
|
||||
final Api _api = locator<Api>();
|
||||
|
||||
Future<CreateApiKeyResponse> postApiKey(
|
||||
String url, String username, String password, String accessLevel, String comment) async {
|
||||
Future<CreateApiKeyResponse> postApiKey(String url, String username,
|
||||
String password, String accessLevel, String comment) async {
|
||||
_api.setUrl(url);
|
||||
|
||||
var fields = Map.fromEntries([
|
||||
|
|
|
@ -24,25 +24,34 @@ class Api implements ApiErrorConverter {
|
|||
String _url = "";
|
||||
String _apiKey = "";
|
||||
|
||||
Map<String, String> _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson};
|
||||
Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit);
|
||||
final Map<String, String> _headers = {
|
||||
"Content-Type": _applicationJson,
|
||||
"Accept": _applicationJson
|
||||
};
|
||||
Duration _timeout = const Duration(seconds: Constants.apiRequestTimeoutLimit);
|
||||
|
||||
Future<http.Response> fetch<T>(String route) async {
|
||||
try {
|
||||
_logger
|
||||
.d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
|
||||
var response = await http.get(Uri.parse(_url + route), headers: _headers).timeout(_timeout);
|
||||
_logger.d(
|
||||
"Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
|
||||
var response = await http
|
||||
.get(Uri.parse(_url + route), headers: _headers)
|
||||
.timeout(_timeout);
|
||||
handleRestErrors(response);
|
||||
return response;
|
||||
} on TimeoutException {
|
||||
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
|
||||
throw ServiceException(
|
||||
code: ErrorCode.socketTimeout, message: _errorTimeout);
|
||||
} on SocketException {
|
||||
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
|
||||
throw ServiceException(
|
||||
code: ErrorCode.socketError, message: _errorNoConnection);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
var uri = Uri.parse(_url + route);
|
||||
var request = http.MultipartRequest('POST', uri)
|
||||
|
@ -58,28 +67,35 @@ class Api implements ApiErrorConverter {
|
|||
}
|
||||
|
||||
if (files != null && files.isNotEmpty) {
|
||||
files.forEach((element) async {
|
||||
request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path));
|
||||
});
|
||||
for (var element in files) {
|
||||
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();
|
||||
additionalFiles.forEach((key, value) {
|
||||
var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1;
|
||||
request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key));
|
||||
var index = files != null
|
||||
? 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 response = await http.Response.fromStream(multiResponse);
|
||||
handleRestErrors(response);
|
||||
return response;
|
||||
} on TimeoutException {
|
||||
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
|
||||
throw ServiceException(
|
||||
code: ErrorCode.socketTimeout, message: _errorTimeout);
|
||||
} on SocketException {
|
||||
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
|
||||
throw ServiceException(
|
||||
code: ErrorCode.socketError, message: _errorNoConnection);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,18 +123,21 @@ class Api implements ApiErrorConverter {
|
|||
/// have a json decoded object. Replace this with a custom
|
||||
/// conversion method by overwriting the interface if needed
|
||||
void handleRestErrors(http.Response response) {
|
||||
if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) {
|
||||
if (response.statusCode != HttpStatus.ok &&
|
||||
response.statusCode != HttpStatus.noContent) {
|
||||
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 &&
|
||||
ContentType.json.subType == responseContentType.subType) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@ import '../datamodels/dialog_request.dart';
|
|||
import '../datamodels/dialog_response.dart';
|
||||
|
||||
class DialogService {
|
||||
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
|
||||
final GlobalKey<NavigatorState> _dialogNavigationKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
late Function(DialogRequest) _showDialogListener;
|
||||
Completer<DialogResponse>? _dialogCompleter;
|
||||
|
||||
|
@ -27,20 +28,28 @@ class DialogService {
|
|||
title: title,
|
||||
description: description,
|
||||
buttonTitleAccept:
|
||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept));
|
||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty
|
||||
? translate('dialog.confirm')
|
||||
: buttonTitleAccept));
|
||||
return _dialogCompleter!.future;
|
||||
}
|
||||
|
||||
Future<DialogResponse> showConfirmationDialog(
|
||||
{String? title, String? description, String? buttonTitleAccept, String? buttonTitleDeny}) {
|
||||
{String? title,
|
||||
String? description,
|
||||
String? buttonTitleAccept,
|
||||
String? buttonTitleDeny}) {
|
||||
_dialogCompleter = Completer<DialogResponse>();
|
||||
_showDialogListener(DialogRequest(
|
||||
title: title,
|
||||
description: description,
|
||||
buttonTitleAccept:
|
||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept,
|
||||
buttonTitleDeny:
|
||||
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny));
|
||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty
|
||||
? translate('dialog.confirm')
|
||||
: buttonTitleAccept,
|
||||
buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
|
||||
? translate('dialog.cancel')
|
||||
: buttonTitleDeny));
|
||||
return _dialogCompleter!.future;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,8 @@ class FileService {
|
|||
return await _fileRepository.postDelete(id);
|
||||
}
|
||||
|
||||
Future<UploadedResponse> uploadPaste(List<File>? files, Map<String, String>? additionalFiles) async {
|
||||
Future<UploadedResponse> uploadPaste(
|
||||
List<File>? files, Map<String, String>? additionalFiles) async {
|
||||
return await _fileRepository.postUpload(files, additionalFiles);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ class LinkService {
|
|||
_logger.e('Could not launch link $link');
|
||||
_dialogService.showDialog(
|
||||
title: translate('link.dialog.title'),
|
||||
description: translate('link.dialog.description', args: {'link': link}));
|
||||
description:
|
||||
translate('link.dialog.description', args: {'link': link}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:logger/logger.dart';
|
|||
import '../util/logger.dart';
|
||||
|
||||
class NavigationService {
|
||||
GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>();
|
||||
final GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>();
|
||||
|
||||
GlobalKey<NavigatorState> get navigationKey => _navigationKey;
|
||||
|
||||
|
@ -17,11 +17,13 @@ class NavigationService {
|
|||
|
||||
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
|
||||
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}) {
|
||||
logger.d('NavigationService: navigateAndReplaceTo $routeName');
|
||||
return _navigationKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
|
||||
return _navigationKey.currentState!
|
||||
.pushReplacementNamed(routeName, arguments: arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,115 +1,109 @@
|
|||
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:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../../constants.dart';
|
||||
import '../../core/datamodels/dialog_response.dart';
|
||||
import '../../core/services/dialog_service.dart';
|
||||
import '../../core/services/stoppable_service.dart';
|
||||
import '../../core/util/logger.dart';
|
||||
import '../../locator.dart';
|
||||
import 'storage_service.dart';
|
||||
|
||||
class PermissionService extends StoppableService {
|
||||
final Logger _logger = getLogger();
|
||||
final DialogService _dialogService = locator<DialogService>();
|
||||
final StorageService _storageService = locator<StorageService>();
|
||||
|
||||
Timer? _serviceCheckTimer;
|
||||
|
||||
PermissionStatus? _permissionStatus;
|
||||
|
||||
bool _permanentlyIgnored = false;
|
||||
bool _devicePermissionDialogActive = false;
|
||||
bool _ownPermissionDialogActive = false;
|
||||
|
||||
PermissionService() {
|
||||
_devicePermissionDialogActive = true;
|
||||
bool _deviceInformationInitialized = false;
|
||||
bool _useStoragePermission = true;
|
||||
|
||||
Permission.storage.request().then((status) {
|
||||
_permissionStatus = status;
|
||||
if (PermissionStatus.permanentlyDenied == status) {
|
||||
_permanentlyIgnored = true;
|
||||
}
|
||||
}).whenComplete(() {
|
||||
_logger.d('Initial device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
}
|
||||
PermissionService();
|
||||
|
||||
Future checkEnabledAndPermission() async {
|
||||
if (_permanentlyIgnored) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
_permanentlyIgnored = false;
|
||||
_logger.d('Set permanently ignored permission request');
|
||||
stop();
|
||||
}
|
||||
|
||||
if (_devicePermissionDialogActive) {
|
||||
_logger.d('Device permission dialog active, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ownPermissionDialogActive) {
|
||||
_logger.d('Own permission dialog already active, skipping');
|
||||
return;
|
||||
bool allGranted = false;
|
||||
bool anyPermanentlyDenied = false;
|
||||
|
||||
// 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();
|
||||
|
||||
if (ignoredDialog) {
|
||||
_logger.d('Permanently ignored permission request, skipping');
|
||||
// show warning to user to manually handle, don't enforce it over and over again
|
||||
if (anyPermanentlyDenied) {
|
||||
_logger.w(
|
||||
"At least one required permission has been denied permanently, stopping service");
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
_permissionStatus = await Permission.storage.status;
|
||||
if (_permissionStatus != PermissionStatus.granted) {
|
||||
if (_permissionStatus == PermissionStatus.permanentlyDenied) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
return;
|
||||
}
|
||||
// all good, stop the permission service
|
||||
if (allGranted) {
|
||||
_logger.d("All permissions have been granted, stopping service");
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
_ownPermissionDialogActive = true;
|
||||
DialogResponse response = await _dialogService.showConfirmationDialog(
|
||||
title: translate('permission_service.dialog.title'),
|
||||
description: translate('permission_service.dialog.description'),
|
||||
buttonTitleAccept: translate('permission_service.dialog.grant'),
|
||||
buttonTitleDeny: translate('permission_service.dialog.ignore'));
|
||||
// not all have been granted, show OS dialog
|
||||
_logger.d(
|
||||
"Not all permissions have been granted yet, initializing permission dialog");
|
||||
_devicePermissionDialogActive = true;
|
||||
|
||||
if (!response.confirmed!) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
} else {
|
||||
_devicePermissionDialogActive = true;
|
||||
Permission.storage.request().then((status) async {
|
||||
if (PermissionStatus.permanentlyDenied == status) {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
}
|
||||
}).whenComplete(() {
|
||||
_logger.d('Device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
}
|
||||
|
||||
_ownPermissionDialogActive = false;
|
||||
if (_useStoragePermission) {
|
||||
await [Permission.storage].request().whenComplete(() {
|
||||
_logger.d('Device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
} else {
|
||||
await _storageService.storeStoragePermissionDialogIgnored();
|
||||
await [Permission.photos, Permission.videos, Permission.audio]
|
||||
.request()
|
||||
.whenComplete(() {
|
||||
_logger.d('Device request permission finished');
|
||||
_devicePermissionDialogActive = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future start() async {
|
||||
super.start();
|
||||
await _determineDeviceInfo();
|
||||
await checkEnabledAndPermission();
|
||||
|
||||
_serviceCheckTimer =
|
||||
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async {
|
||||
_serviceCheckTimer = Timer.periodic(
|
||||
const Duration(milliseconds: Constants.mediaPermissionCheckInterval),
|
||||
(serviceTimer) async {
|
||||
if (!super.serviceStopped) {
|
||||
await checkEnabledAndPermission();
|
||||
} else {
|
||||
_serviceTimer.cancel();
|
||||
serviceTimer.cancel();
|
||||
}
|
||||
});
|
||||
_logger.d('PermissionService started');
|
||||
|
@ -122,6 +116,29 @@ class PermissionService extends StoppableService {
|
|||
_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() {
|
||||
if (_serviceCheckTimer != null) {
|
||||
_serviceCheckTimer!.cancel();
|
||||
|
|
|
@ -3,7 +3,8 @@ import 'dart:async';
|
|||
import '../enums/refresh_event.dart';
|
||||
|
||||
class RefreshService {
|
||||
StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent>.broadcast();
|
||||
StreamController<RefreshEvent> refreshEventController =
|
||||
StreamController<RefreshEvent>.broadcast();
|
||||
|
||||
void addEvent(RefreshEvent event) {
|
||||
if (refreshEventController.hasListener) {
|
||||
|
|
|
@ -31,7 +31,7 @@ class SessionService extends StoppableService {
|
|||
Future<bool> login(String url, String apiKey) async {
|
||||
setApiConfig(url, apiKey);
|
||||
|
||||
var session = new Session(url: url, apiKey: apiKey);
|
||||
var session = Session(url: url, apiKey: apiKey);
|
||||
sessionController.add(session);
|
||||
await _storageService.storeSession(session);
|
||||
_logger.d('Session created');
|
||||
|
|
|
@ -5,45 +5,36 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||
import '../models/session.dart';
|
||||
|
||||
class StorageService {
|
||||
static const _SESSION_KEY = 'session';
|
||||
static const _LAST_URL_KEY = 'last_url';
|
||||
static const _STORAGE_PERMISSION_DIALOG_IGNORED = 'storage_permission_ignored';
|
||||
static const _sessionKey = 'session';
|
||||
static const _lastUrlKey = 'last_url';
|
||||
|
||||
Future<bool> storeLastUrl(String url) {
|
||||
return _store(_LAST_URL_KEY, url);
|
||||
return _store(_lastUrlKey, url);
|
||||
}
|
||||
|
||||
Future<String?> retrieveLastUrl() async {
|
||||
return await _retrieve(_LAST_URL_KEY);
|
||||
return await _retrieve(_lastUrlKey);
|
||||
}
|
||||
|
||||
Future<bool> hasLastUrl() async {
|
||||
return await _exists(_LAST_URL_KEY);
|
||||
return await _exists(_lastUrlKey);
|
||||
}
|
||||
|
||||
Future<bool> storeSession(Session session) {
|
||||
return _store(_SESSION_KEY, json.encode(session));
|
||||
return _store(_sessionKey, json.encode(session));
|
||||
}
|
||||
|
||||
Future<Session> retrieveSession() async {
|
||||
var retrieve = await _retrieve(_SESSION_KEY);
|
||||
var retrieve = await _retrieve(_sessionKey);
|
||||
return Session.fromJson(json.decode(retrieve!));
|
||||
}
|
||||
|
||||
Future<bool> hasSession() {
|
||||
return _exists(_SESSION_KEY);
|
||||
return _exists(_sessionKey);
|
||||
}
|
||||
|
||||
Future<bool> removeSession() {
|
||||
return _remove(_SESSION_KEY);
|
||||
}
|
||||
|
||||
Future<bool> storeStoragePermissionDialogIgnored() {
|
||||
return _store(_STORAGE_PERMISSION_DIALOG_IGNORED, true.toString());
|
||||
}
|
||||
|
||||
Future<bool> hasStoragePermissionDialogIgnored() {
|
||||
return _exists(_STORAGE_PERMISSION_DIALOG_IGNORED);
|
||||
return _remove(_sessionKey);
|
||||
}
|
||||
|
||||
Future<bool> _exists(String key) async {
|
||||
|
|
|
@ -12,9 +12,10 @@ class UserService {
|
|||
final FileService _fileService = locator<FileService>();
|
||||
final UserRepository _userRepository = locator<UserRepository>();
|
||||
|
||||
Future<CreateApiKeyResponse> createApiKey(
|
||||
String url, String username, String password, String accessLevel, String comment) async {
|
||||
return await _userRepository.postApiKey(url, username, password, accessLevel, comment);
|
||||
Future<CreateApiKeyResponse> createApiKey(String url, String username,
|
||||
String password, String accessLevel, String comment) async {
|
||||
return await _userRepository.postApiKey(
|
||||
url, username, password, accessLevel, comment);
|
||||
}
|
||||
|
||||
Future<ApiKeysResponse> getApiKeys() async {
|
||||
|
@ -26,7 +27,7 @@ class UserService {
|
|||
try {
|
||||
await _fileService.getHistory();
|
||||
} on ServiceException catch (e) {
|
||||
throw new ServiceException(code: ErrorCode.INVALID_API_KEY, message: e.message);
|
||||
throw ServiceException(code: ErrorCode.invalidApiKey, message: e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,14 @@ class FormatterUtil {
|
|||
/// Format epoch timestamp
|
||||
static String formatEpoch(num millis) {
|
||||
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm();
|
||||
return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
|
||||
return dateFormat
|
||||
.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
|
||||
}
|
||||
|
||||
static String formatBytes(int bytes, int decimals) {
|
||||
if (bytes <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
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]}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ class AboutModel extends BaseModel {
|
|||
}
|
||||
|
||||
Future<void> _initPackageInfo() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
final PackageInfo info = await PackageInfo.fromPlatform();
|
||||
packageInfo = info;
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
void openLink(String link) {
|
||||
|
|
|
@ -5,18 +5,21 @@ import '../../core/util/logger.dart';
|
|||
import '../enums/viewstate.dart';
|
||||
|
||||
class BaseModel extends ChangeNotifier {
|
||||
static const String STATE_VIEW = 'viewState';
|
||||
static const String STATE_MESSAGE = 'viewMessage';
|
||||
static const String stateViewKey = 'viewState';
|
||||
static const String stateMessageKey = 'viewMessage';
|
||||
|
||||
final Logger _logger = getLogger();
|
||||
|
||||
bool _isDisposed = false;
|
||||
|
||||
Map<String, Object?> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
|
||||
final Map<String, Object?> _stateMap = {
|
||||
stateViewKey: ViewState.idle,
|
||||
stateMessageKey: null
|
||||
};
|
||||
|
||||
ViewState? get state => _stateMap[STATE_VIEW] as ViewState?;
|
||||
ViewState? get state => _stateMap[stateViewKey] as ViewState?;
|
||||
|
||||
String? get stateMessage => _stateMap[STATE_MESSAGE] as String?;
|
||||
String? get stateMessage => _stateMap[stateMessageKey] as String?;
|
||||
|
||||
bool getStateValueAsBoolean(String key) {
|
||||
if (_stateMap.containsKey(key) && _stateMap[key] is bool) {
|
||||
|
@ -42,11 +45,14 @@ class BaseModel extends ChangeNotifier {
|
|||
return null;
|
||||
}
|
||||
|
||||
void setStateBoolValue(String key, bool stateValue) => _setStateValue(key, stateValue);
|
||||
void setStateBoolValue(String key, bool stateValue) =>
|
||||
_setStateValue(key, stateValue);
|
||||
|
||||
void setStateIntValue(String key, int? stateValue) => _setStateValue(key, stateValue);
|
||||
void setStateIntValue(String key, int? stateValue) =>
|
||||
_setStateValue(key, stateValue);
|
||||
|
||||
void setStateStringValue(String key, String? stateValue) => _setStateValue(key, stateValue);
|
||||
void setStateStringValue(String key, String? stateValue) =>
|
||||
_setStateValue(key, stateValue);
|
||||
|
||||
void _setStateValue(String key, Object? stateValue) {
|
||||
if (_stateMap.containsKey(key)) {
|
||||
|
@ -57,7 +63,8 @@ class BaseModel extends ChangeNotifier {
|
|||
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
_logger.d("Notified state value update '($key, ${stateValue.toString()})'");
|
||||
_logger
|
||||
.d("Notified state value update '($key, ${stateValue.toString()})'");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,15 +78,16 @@ class BaseModel extends ChangeNotifier {
|
|||
}
|
||||
|
||||
void setStateView(ViewState stateView) {
|
||||
_setStateValue(STATE_VIEW, stateView);
|
||||
_setStateValue(stateViewKey, stateView);
|
||||
}
|
||||
|
||||
void setStateMessage(String? stateMessage) {
|
||||
_setStateValue(STATE_MESSAGE, stateMessage);
|
||||
_setStateValue(stateMessageKey, stateMessage);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logger.d("Calling dispose");
|
||||
super.dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
|
|
|
@ -34,8 +34,9 @@ class HistoryModel extends BaseModel {
|
|||
String? errorMessage;
|
||||
|
||||
void init() {
|
||||
_refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
|
||||
if (event == RefreshEvent.RefreshHistory) {
|
||||
_refreshTriggerSubscription =
|
||||
_refreshService.refreshEventController.stream.listen((event) {
|
||||
if (event == RefreshEvent.refreshHistory) {
|
||||
_logger.d('History needs a refresh');
|
||||
getHistory();
|
||||
}
|
||||
|
@ -43,18 +44,19 @@ class HistoryModel extends BaseModel {
|
|||
}
|
||||
|
||||
Future getHistory() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
|
||||
try {
|
||||
pastes.clear();
|
||||
History _history = await _fileService.getHistory();
|
||||
if (_history.items.isNotEmpty) {
|
||||
_history.items.forEach((key, value) {
|
||||
History history = await _fileService.getHistory();
|
||||
if (history.items.isNotEmpty) {
|
||||
history.items.forEach((key, value) {
|
||||
var millisecondsSinceEpoch = int.parse(value.date) * 1000;
|
||||
pastes.add(
|
||||
UploadedPaste(
|
||||
id: key,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
|
||||
date:
|
||||
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
|
||||
filename: value.filename,
|
||||
filesize: int.parse(value.filesize),
|
||||
hash: value.hash,
|
||||
|
@ -66,8 +68,8 @@ class HistoryModel extends BaseModel {
|
|||
});
|
||||
}
|
||||
|
||||
if (_history.multipasteItems.isNotEmpty) {
|
||||
_history.multipasteItems.forEach((key, multiPaste) {
|
||||
if (history.multipasteItems.isNotEmpty) {
|
||||
history.multipasteItems.forEach((key, multiPaste) {
|
||||
var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000;
|
||||
pastes.add(UploadedPaste(
|
||||
id: key,
|
||||
|
@ -90,32 +92,35 @@ class HistoryModel extends BaseModel {
|
|||
e.responseBody is RestError &&
|
||||
e.responseBody.message != null) {
|
||||
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 {
|
||||
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 {
|
||||
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');
|
||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||
errorMessage = translate('api.socket_timeout');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
setStateView(ViewState.idle);
|
||||
_logger.e('An unknown error occurred', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
Future deletePaste(String id) async {
|
||||
DialogResponse res = await _dialogService.showConfirmationDialog(
|
||||
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'),
|
||||
buttonTitleDeny: translate('history.delete_dialog.deny'));
|
||||
|
||||
|
@ -123,7 +128,7 @@ class HistoryModel extends BaseModel {
|
|||
return;
|
||||
}
|
||||
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
|
||||
try {
|
||||
await _fileService.deletePaste(id);
|
||||
|
@ -139,23 +144,24 @@ class HistoryModel extends BaseModel {
|
|||
e.statusCode != HttpStatus.forbidden &&
|
||||
e.responseBody is RestError &&
|
||||
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 {
|
||||
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');
|
||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||
errorMessage = translate('api.socket_timeout');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
setStateView(ViewState.idle);
|
||||
_logger.e('An unknown error occurred', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
void openLink(String link) {
|
||||
|
|
|
@ -20,12 +20,12 @@ import '../util/logger.dart';
|
|||
import 'base_model.dart';
|
||||
|
||||
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 = new TextEditingController();
|
||||
final TextEditingController _apiKeyController = TextEditingController();
|
||||
|
||||
TextEditingController get uriController => _uriController;
|
||||
|
||||
|
@ -44,23 +44,23 @@ class LoginModel extends BaseModel {
|
|||
String? errorMessage;
|
||||
|
||||
void toggleLoginMethod() {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
useCredentialsLogin = !useCredentialsLogin;
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
void init() async {
|
||||
bool hasLastUrl = await _storageService.hasLastUrl();
|
||||
|
||||
if (hasLastUrl) {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
|
||||
|
||||
if (s.isNotEmpty) {
|
||||
_uriController = new TextEditingController(text: s);
|
||||
_uriController = TextEditingController(text: s);
|
||||
}
|
||||
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,45 +70,45 @@ class LoginModel extends BaseModel {
|
|||
var password = passwordController.text;
|
||||
var apiKey = apiKeyController.text;
|
||||
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
url = trim(url);
|
||||
username = trim(username);
|
||||
|
||||
if (url.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_url');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!url.contains("https://") && !url.contains("http://")) {
|
||||
errorMessage = translate('login.errors.no_protocol');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool validUri = Uri.parse(url).isAbsolute;
|
||||
if (!validUri || !isURL(url)) {
|
||||
errorMessage = translate('login.errors.invalid_url');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (useCredentialsLogin) {
|
||||
if (username.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_username');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_password');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (apiKey.isEmpty) {
|
||||
errorMessage = translate('login.errors.empty_apikey');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -117,14 +117,19 @@ class LoginModel extends BaseModel {
|
|||
try {
|
||||
if (useCredentialsLogin) {
|
||||
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
|
||||
url, username, password, 'apikey', 'fbmobile-${new DateTime.now().millisecondsSinceEpoch}');
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
'apikey',
|
||||
'fbmobile-${DateTime.now().millisecondsSinceEpoch}');
|
||||
|
||||
var newKey = apiKeyResponse.data['new_key'];
|
||||
if (newKey != null) {
|
||||
success = await _sessionService.login(url, newKey);
|
||||
} else {
|
||||
throw new ServiceException(
|
||||
code: ErrorCode.INVALID_API_KEY, message: translate('login.errors.invalid_api_key'));
|
||||
throw ServiceException(
|
||||
code: ErrorCode.invalidApiKey,
|
||||
message: translate('login.errors.invalid_api_key'));
|
||||
}
|
||||
} else {
|
||||
_sessionService.setApiConfig(url, apiKey);
|
||||
|
@ -136,35 +141,37 @@ class LoginModel extends BaseModel {
|
|||
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) {
|
||||
} 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});
|
||||
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.INVALID_API_KEY) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.invalidApiKey) {
|
||||
errorMessage = translate('login.errors.invalid_api_key');
|
||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||
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');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
_sessionService.logout();
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
setStateView(ViewState.idle);
|
||||
_logger.e('An unknown error occurred', error: e);
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (errorMessage!.isNotEmpty) {
|
||||
_sessionService.logout();
|
||||
}
|
||||
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return success;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ class ProfileModel extends BaseModel {
|
|||
|
||||
Future logout() async {
|
||||
var dialogResult = await _dialogService.showConfirmationDialog(
|
||||
title: translate('logout.title'), description: translate('logout.confirm'));
|
||||
title: translate('logout.title'),
|
||||
description: translate('logout.confirm'));
|
||||
|
||||
if (dialogResult.confirmed!) {
|
||||
await _sessionService.logout();
|
||||
|
@ -41,7 +42,8 @@ class ProfileModel extends BaseModel {
|
|||
Future revealApiKey(String? apiKey) async {
|
||||
await _dialogService.showDialog(
|
||||
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 {
|
||||
|
@ -54,27 +56,29 @@ class ProfileModel extends BaseModel {
|
|||
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) {
|
||||
} 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});
|
||||
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.SOCKET_ERROR) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||
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');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setStateBoolValue(_configurationButtonLoading, false);
|
||||
_sessionService.logout();
|
||||
setStateBoolValue(_configurationButtonLoading, false);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
_logger.e('An unknown error occurred', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,15 +88,18 @@ class ProfileModel extends BaseModel {
|
|||
await _dialogService.showDialog(
|
||||
title: translate('profile.shown_config.title'),
|
||||
description: translate('profile.shown_config.description', args: {
|
||||
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
|
||||
'uploadMaxSize':
|
||||
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
|
||||
'maxFilesPerRequest': config.maxFilesPerRequest,
|
||||
'maxInputVars': config.maxInputVars,
|
||||
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
|
||||
'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}));
|
||||
description: translate('profile.shown_config.error.description',
|
||||
args: {'message': errorMessage}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fbmobile/core/services/permission_service.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
import '../../locator.dart';
|
||||
|
@ -9,19 +10,21 @@ import 'base_model.dart';
|
|||
|
||||
class StartUpViewModel extends BaseModel {
|
||||
final SessionService _sessionService = locator<SessionService>();
|
||||
final PermissionService _permissionService = locator<PermissionService>();
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
|
||||
Future handleStartUpLogic() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
setStateMessage(translate('startup.init'));
|
||||
await Future.delayed(Duration(milliseconds: 150));
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
setStateMessage(translate('startup.start_services'));
|
||||
await _sessionService.start();
|
||||
await Future.delayed(Duration(milliseconds: 150));
|
||||
await _permissionService.start();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@ import 'dart:io';
|
|||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.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:logger/logger.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
|
||||
import '../../locator.dart';
|
||||
import '../enums/error_code.dart';
|
||||
|
@ -31,7 +32,7 @@ class UploadModel extends BaseModel {
|
|||
final LinkService _linkService = locator<LinkService>();
|
||||
final RefreshService _refreshService = locator<RefreshService>();
|
||||
|
||||
TextEditingController _pasteTextController = TextEditingController();
|
||||
final TextEditingController _pasteTextController = TextEditingController();
|
||||
bool pasteTextTouched = false;
|
||||
|
||||
late StreamSubscription _intentDataStreamSubscription;
|
||||
|
@ -45,6 +46,41 @@ class UploadModel extends BaseModel {
|
|||
|
||||
TextEditingController get pasteTextController => _pasteTextController;
|
||||
|
||||
void _parseIntentFiles(List<SharedFile> files) {
|
||||
if (files.isNotEmpty) {
|
||||
setStateView(ViewState.busy);
|
||||
|
||||
paths = files.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({
|
||||
'path': sharedFile.value,
|
||||
'name': basename(sharedFile.value!),
|
||||
'size': File(sharedFile.value!).lengthSync(),
|
||||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
|
||||
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;
|
||||
|
@ -52,73 +88,33 @@ class UploadModel extends BaseModel {
|
|||
});
|
||||
|
||||
// For sharing images coming from outside the app while the app is in the memory
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
||||
if (value.length > 0) {
|
||||
setStateView(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
'name': basename(sharedFile.path),
|
||||
'size': File(sharedFile.path).lengthSync(),
|
||||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
_intentDataStreamSubscription = FlutterSharingIntent.instance
|
||||
.getMediaStream()
|
||||
.listen((List<SharedFile> value) {
|
||||
_logger.d("Retrieved ${value.length} files from intent");
|
||||
_parseIntentFiles(value);
|
||||
}, onError: (err) {
|
||||
_errorIntentHandle(err);
|
||||
});
|
||||
|
||||
// For sharing images coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
||||
if (value.length > 0) {
|
||||
setStateView(ViewState.Busy);
|
||||
paths = value.map((sharedFile) {
|
||||
return PlatformFile.fromMap({
|
||||
'path': sharedFile.path,
|
||||
'name': basename(sharedFile.path),
|
||||
'size': File(sharedFile.path).lengthSync(),
|
||||
'bytes': null
|
||||
});
|
||||
}).toList();
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
_errorIntentHandle(err);
|
||||
});
|
||||
|
||||
// For sharing or opening urls/text coming from outside the app while the app is in the memory
|
||||
_intentDataStreamSubscription = ReceiveSharingIntent.getTextStream().listen((String value) {
|
||||
if (value.isNotEmpty) {
|
||||
setStateView(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
_errorIntentHandle(err);
|
||||
});
|
||||
|
||||
// For sharing or opening urls/text coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.getInitialText().then((String? value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
setStateView(ViewState.Busy);
|
||||
pasteTextController.text = value;
|
||||
setStateView(ViewState.Idle);
|
||||
}
|
||||
}, onError: (err) {
|
||||
_errorIntentHandle(err);
|
||||
FlutterSharingIntent.instance
|
||||
.getInitialSharing()
|
||||
.then((List<SharedFile> value) {
|
||||
_logger.d("Retrieved ${value.length} files from inactive intent");
|
||||
_parseIntentFiles(value);
|
||||
});
|
||||
}
|
||||
|
||||
void _errorIntentHandle(err) {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
errorMessage = translate('upload.retrieval_intent');
|
||||
_logger.e('Error while retrieving shared data: $err');
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
String? generatePasteLinks(Map<String, bool>? uploads, String url) {
|
||||
if (uploads != null && uploads.length > 0) {
|
||||
if (uploads != null && uploads.isNotEmpty) {
|
||||
var links = '';
|
||||
|
||||
uploads.forEach((id, isMulti) {
|
||||
|
@ -134,13 +130,13 @@ class UploadModel extends BaseModel {
|
|||
}
|
||||
|
||||
void toggleCreateMulti() {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
createMulti = !createMulti;
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
void openFileExplorer() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
setStateMessage(translate('upload.file_explorer_open'));
|
||||
loadingPath = true;
|
||||
|
||||
|
@ -150,62 +146,68 @@ class UploadModel extends BaseModel {
|
|||
allowMultiple: true,
|
||||
withData: false,
|
||||
withReadStream: true,
|
||||
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '').split(',') : null,
|
||||
allowedExtensions: (_extension?.isNotEmpty ?? false)
|
||||
? _extension?.replaceAll(' ', '').split(',')
|
||||
: null,
|
||||
))
|
||||
?.files;
|
||||
} on PlatformException catch (e) {
|
||||
_logger.e('Unsupported operation', e);
|
||||
_logger.e('Unsupported operation', error: e);
|
||||
} catch (ex) {
|
||||
_logger.e('An unknown error occurred', ex);
|
||||
_logger.e('An unknown error occurred', error: ex);
|
||||
}
|
||||
|
||||
loadingPath = false;
|
||||
fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
|
||||
|
||||
setStateMessage(null);
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
void clearCachedFiles() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
await FilePicker.platform.clearTemporaryFiles();
|
||||
paths = null;
|
||||
fileName = null;
|
||||
errorMessage = null;
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
}
|
||||
|
||||
Future<Map<String, bool>?> upload() async {
|
||||
setStateView(ViewState.Busy);
|
||||
setStateView(ViewState.busy);
|
||||
setStateMessage(translate('upload.uploading_now'));
|
||||
|
||||
Map<String, bool> uploadedPasteIds = new Map();
|
||||
Map<String, bool> uploadedPasteIds = {};
|
||||
try {
|
||||
List<File>? files;
|
||||
Map<String, String>? additionalFiles;
|
||||
|
||||
if (pasteTextController.text.isNotEmpty) {
|
||||
additionalFiles = Map.from(
|
||||
{'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text});
|
||||
additionalFiles = Map.from({
|
||||
'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
|
||||
pasteTextController.text
|
||||
});
|
||||
}
|
||||
|
||||
if (paths != null && paths!.length > 0) {
|
||||
files = paths!.map((e) => new File(e.path!)).toList();
|
||||
if (paths != null && paths!.isNotEmpty) {
|
||||
files = paths!.map((e) => File(e.path!)).toList();
|
||||
}
|
||||
|
||||
UploadedResponse response = await _fileService.uploadPaste(files, additionalFiles);
|
||||
response.data.ids.forEach((element) {
|
||||
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) {
|
||||
UploadedMultiResponse multiResponse = await _fileService.uploadMultiPaste(response.data.ids);
|
||||
UploadedMultiResponse multiResponse =
|
||||
await _fileService.uploadMultiPaste(response.data.ids);
|
||||
uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true);
|
||||
}
|
||||
|
||||
clearCachedFiles();
|
||||
_pasteTextController.clear();
|
||||
_refreshService.addEvent(RefreshEvent.RefreshHistory);
|
||||
_refreshService.addEvent(RefreshEvent.refreshHistory);
|
||||
errorMessage = null;
|
||||
return uploadedPasteIds;
|
||||
} catch (e) {
|
||||
|
@ -219,28 +221,30 @@ class UploadModel extends BaseModel {
|
|||
e.responseBody is RestError &&
|
||||
e.responseBody.message != null) {
|
||||
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 {
|
||||
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 {
|
||||
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');
|
||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
||||
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||
errorMessage = translate('api.socket_timeout');
|
||||
} else {
|
||||
errorMessage = translate('app.unknown_error');
|
||||
setStateMessage(null);
|
||||
setStateView(ViewState.Idle);
|
||||
_logger.e('An unknown error occurred', e);
|
||||
throw e;
|
||||
setStateView(ViewState.idle);
|
||||
_logger.e('An unknown error occurred', error: e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
setStateMessage(null);
|
||||
setStateView(ViewState.Idle);
|
||||
setStateView(ViewState.idle);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -252,6 +256,8 @@ class UploadModel extends BaseModel {
|
|||
void dispose() {
|
||||
_pasteTextController.dispose();
|
||||
_intentDataStreamSubscription.cancel();
|
||||
FlutterSharingIntent.instance.reset();
|
||||
paths = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ void main() async {
|
|||
setupLogger(Level.info);
|
||||
setupLocator();
|
||||
|
||||
var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
|
||||
var delegate = await LocalizationDelegate.create(
|
||||
fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(LocalizedApp(delegate, MyApp()));
|
||||
|
|
|
@ -14,20 +14,21 @@ class AppRouter {
|
|||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case StartUpView.routeName:
|
||||
return MaterialPageRoute(builder: (_) => StartUpView());
|
||||
return MaterialPageRoute(builder: (_) => const StartUpView());
|
||||
case AboutView.routeName:
|
||||
return MaterialPageRoute(builder: (_) => AboutView());
|
||||
return MaterialPageRoute(builder: (_) => const AboutView());
|
||||
case HomeView.routeName:
|
||||
return MaterialPageRoute(builder: (_) => TabBarContainerView());
|
||||
return MaterialPageRoute(builder: (_) => const TabBarContainerView());
|
||||
case LoginView.routeName:
|
||||
return MaterialPageRoute(builder: (_) => LoginView());
|
||||
case ProfileView.routeName:
|
||||
return MaterialPageRoute(builder: (_) => ProfileView());
|
||||
return MaterialPageRoute(builder: (_) => const ProfileView());
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(
|
||||
child: Text(translate('dev.no_route', args: {'route': settings.name})),
|
||||
child: Text(translate('dev.no_route',
|
||||
args: {'route': settings.name})),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class UIHelper {
|
||||
static const double _VerticalSpaceSmall = 10.0;
|
||||
static const double _VerticalSpaceMedium = 20.0;
|
||||
static const double _VerticalSpaceLarge = 60.0;
|
||||
static const double _verticalSpaceSmall = 10.0;
|
||||
static const double _verticalSpaceMedium = 20.0;
|
||||
static const double _verticalSpaceLarge = 60.0;
|
||||
|
||||
static const double _HorizontalSpaceSmall = 10.0;
|
||||
static const double _HorizontalSpaceMedium = 20.0;
|
||||
static const double HorizontalSpaceLarge = 60.0;
|
||||
static const double _horizontalSpaceSmall = 10.0;
|
||||
static const double _horizontalSpaceMedium = 20.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() {
|
||||
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() {
|
||||
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() {
|
||||
return verticalSpace(_VerticalSpaceLarge);
|
||||
return verticalSpace(_verticalSpaceLarge);
|
||||
}
|
||||
|
||||
/// Returns a vertical space equal to the [height] supplied
|
||||
|
@ -29,19 +29,19 @@ class UIHelper {
|
|||
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() {
|
||||
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() {
|
||||
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() {
|
||||
return horizontalSpace(HorizontalSpaceLarge);
|
||||
return horizontalSpace(_horizontalSpaceLarge);
|
||||
}
|
||||
|
||||
/// Returns a vertical space equal to the [width] supplied
|
||||
|
|
|
@ -12,6 +12,8 @@ import 'base_view.dart';
|
|||
class AboutView extends StatelessWidget {
|
||||
static const routeName = '/about';
|
||||
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logo = Hero(
|
||||
|
@ -30,13 +32,14 @@ class AboutView extends StatelessWidget {
|
|||
title: Text(translate('titles.about')),
|
||||
enableAbout: false,
|
||||
),
|
||||
body: model.state == ViewState.Busy
|
||||
? Center(child: CircularProgressIndicator())
|
||||
body: model.state == ViewState.busy
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Container(
|
||||
padding: EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10, top: 10),
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0, right: 10.0, bottom: 10, top: 10),
|
||||
children: <Widget>[
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(child: logo),
|
||||
|
@ -75,7 +78,7 @@ class AboutView extends StatelessWidget {
|
|||
Center(
|
||||
child: Linkify(
|
||||
text: translate('about.website'),
|
||||
options: LinkifyOptions(humanize: false),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
onOpen: (link) => model.openLink(link.url),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ class BaseView<T extends BaseModel> extends StatefulWidget {
|
|||
final Widget Function(BuildContext context, T model, Widget? child)? builder;
|
||||
final Function(T)? onModelReady;
|
||||
|
||||
BaseView({this.builder, this.onModelReady});
|
||||
const BaseView({super.key, this.builder, this.onModelReady});
|
||||
|
||||
@override
|
||||
_BaseViewState<T> createState() => _BaseViewState<T>();
|
||||
|
@ -31,7 +31,9 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
|
|||
|
||||
@override
|
||||
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
|
||||
|
|
|
@ -18,6 +18,8 @@ import 'base_view.dart';
|
|||
class HistoryView extends StatelessWidget {
|
||||
static const routeName = '/history';
|
||||
|
||||
const HistoryView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseView<HistoryModel>(
|
||||
|
@ -25,23 +27,25 @@ class HistoryView extends StatelessWidget {
|
|||
model.init();
|
||||
return model.getHistory();
|
||||
},
|
||||
builder: (context, model, child) =>
|
||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.history'))), body: _render(model, context)),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.history'))),
|
||||
body: _render(model, context)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _render(HistoryModel model, BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
|
||||
return model.state == ViewState.Busy
|
||||
? Center(child: CircularProgressIndicator())
|
||||
return model.state == ViewState.busy
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: (model.errorMessage == null
|
||||
? Container(
|
||||
padding: EdgeInsets.all(0),
|
||||
padding: const EdgeInsets.all(0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async => await model.getHistory(), child: _renderItems(model, url, context)))
|
||||
onRefresh: () async => await model.getHistory(),
|
||||
child: _renderItems(model, url, context)))
|
||||
: Container(
|
||||
padding: EdgeInsets.all(25),
|
||||
padding: const EdgeInsets.all(25),
|
||||
child: CenteredErrorRow(
|
||||
model.errorMessage,
|
||||
retryCallback: () => model.getHistory(),
|
||||
|
@ -51,15 +55,16 @@ class HistoryView extends StatelessWidget {
|
|||
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
|
||||
List<Widget> cards = [];
|
||||
|
||||
if (model.pastes.length > 0) {
|
||||
model.pastes.reversed.forEach((paste) {
|
||||
if (model.pastes.isNotEmpty) {
|
||||
for (var paste in model.pastes.reversed) {
|
||||
List<Widget> widgets = [];
|
||||
|
||||
var fullPasteUrl = PasteUtil.generateLink(url, paste.id);
|
||||
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
|
||||
|
||||
var dateWidget = ListTile(
|
||||
title: Text(FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
|
||||
title: Text(
|
||||
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
|
||||
subtitle: Text(translate('history.date')),
|
||||
);
|
||||
|
||||
|
@ -71,7 +76,8 @@ class HistoryView extends StatelessWidget {
|
|||
var copyWidget = ListTile(
|
||||
title: Text(translate('history.copy_link.description')),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
|
||||
icon: const Icon(Icons.copy,
|
||||
color: blueColor, textDirection: TextDirection.ltr),
|
||||
onPressed: () {
|
||||
FlutterClipboard.copy(fullPasteUrl).then((value) {
|
||||
final snackBar = SnackBar(
|
||||
|
@ -83,16 +89,18 @@ class HistoryView extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
content: Text(translate('history.copy_link.copied')),
|
||||
duration: Duration(seconds: 10),
|
||||
duration: const Duration(seconds: 10),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
var deleteWidget = ListTile(
|
||||
title: Text(translate('history.delete')),
|
||||
trailing: IconButton(
|
||||
icon: Icon(Icons.delete, color: redColor),
|
||||
icon: const Icon(Icons.delete, color: redColor),
|
||||
onPressed: () {
|
||||
model.deletePaste(paste.id);
|
||||
}));
|
||||
|
@ -120,13 +128,13 @@ class HistoryView extends StatelessWidget {
|
|||
widgets.add(fileSizeWidget);
|
||||
widgets.add(mimeTypeWidget);
|
||||
} else {
|
||||
paste.items!.forEach((element) {
|
||||
for (var element in paste.items!) {
|
||||
widgets.add(ListTile(
|
||||
title: Text(element!),
|
||||
subtitle: Text(translate('history.multipaste_element')),
|
||||
trailing: _renderOpenInBrowser(model, '$url/$element'),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
widgets.add(dateWidget);
|
||||
|
@ -135,7 +143,7 @@ class HistoryView extends StatelessWidget {
|
|||
widgets.add(deleteWidget);
|
||||
|
||||
var expandable = ExpandableTheme(
|
||||
data: ExpandableThemeData(
|
||||
data: const ExpandableThemeData(
|
||||
iconPlacement: ExpandablePanelIconPlacement.right,
|
||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||
hasIcon: true,
|
||||
|
@ -146,7 +154,7 @@ class HistoryView extends StatelessWidget {
|
|||
header: InkWell(
|
||||
child: Text(
|
||||
paste.id,
|
||||
style: TextStyle(color: blueColor),
|
||||
style: const TextStyle(color: blueColor),
|
||||
textAlign: TextAlign.left,
|
||||
)),
|
||||
expanded: Column(
|
||||
|
@ -162,15 +170,17 @@ class HistoryView extends StatelessWidget {
|
|||
trailing: Wrap(children: [
|
||||
openInBrowserButton,
|
||||
IconButton(
|
||||
icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
|
||||
icon: const Icon(Icons.share,
|
||||
color: blueColor, textDirection: TextDirection.ltr),
|
||||
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 {
|
||||
cards.add(Card(
|
||||
child: ListTile(
|
||||
|
@ -181,14 +191,15 @@ class HistoryView extends StatelessWidget {
|
|||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(8),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: cards,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _renderOpenInBrowser(HistoryModel model, String url) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr),
|
||||
icon: const Icon(Icons.open_in_new,
|
||||
color: blueColor, textDirection: TextDirection.ltr),
|
||||
onPressed: () {
|
||||
return model.openLink(url);
|
||||
});
|
||||
|
|
|
@ -9,12 +9,16 @@ import 'base_view.dart';
|
|||
class HomeView extends StatelessWidget {
|
||||
static const routeName = '/home';
|
||||
|
||||
const HomeView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseView<HomeModel>(
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('app.title'))),
|
||||
body: model.state == ViewState.Busy ? Center(child: CircularProgressIndicator()) : Container()),
|
||||
body: model.state == ViewState.busy
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Container()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import '../../core/services/dialog_service.dart';
|
|||
import '../../core/services/navigation_service.dart';
|
||||
import '../../core/viewmodels/login_model.dart';
|
||||
import '../../locator.dart';
|
||||
import '../../ui/shared/text_styles.dart';
|
||||
import '../../ui/views/home_view.dart';
|
||||
import '../../ui/widgets/my_appbar.dart';
|
||||
import '../shared/app_colors.dart';
|
||||
|
@ -21,6 +20,8 @@ class LoginView extends StatelessWidget {
|
|||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
final DialogService _dialogService = locator<DialogService>();
|
||||
|
||||
LoginView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final logo = Hero(
|
||||
|
@ -36,11 +37,11 @@ class LoginView extends StatelessWidget {
|
|||
onModelReady: (model) => model.init(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.login'))),
|
||||
body: model.state == ViewState.Busy
|
||||
? Center(child: CircularProgressIndicator())
|
||||
body: model.state == ViewState.busy
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.only(left: 10.0, right: 10.0),
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
|
||||
children: <Widget>[
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Center(child: logo),
|
||||
|
@ -50,21 +51,22 @@ class LoginView extends StatelessWidget {
|
|||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
alignment: WrapAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
translate('login.help'),
|
||||
style: subHeaderStyle,
|
||||
),
|
||||
InkWell(
|
||||
child: Icon(Icons.help),
|
||||
child: const Icon(Icons.help),
|
||||
onTap: () {
|
||||
_dialogService.showDialog(
|
||||
title: translate('login.compatibility_dialog.title'),
|
||||
description: translate('login.compatibility_dialog.body'));
|
||||
title: translate(
|
||||
'login.compatibility_dialog.title'),
|
||||
description: translate(
|
||||
'login.compatibility_dialog.body'));
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
child:
|
||||
Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor),
|
||||
child: Icon(
|
||||
model.useCredentialsLogin
|
||||
? Icons.person_outline
|
||||
: Icons.vpn_key,
|
||||
color: blueColor),
|
||||
onTap: () {
|
||||
model.toggleLoginMethod();
|
||||
},
|
||||
|
@ -83,12 +85,14 @@ class LoginView extends StatelessWidget {
|
|||
uriController: model.uriController,
|
||||
apiKeyController: model.apiKeyController),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
ElevatedButton(
|
||||
child: Text(translate('login.button')),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.login, color: blueColor),
|
||||
label: Text(translate('login.button')),
|
||||
onPressed: () async {
|
||||
var loginSuccess = await model.login();
|
||||
if (loginSuccess) {
|
||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||
_navigationService
|
||||
.navigateAndReplaceTo(HomeView.routeName);
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
@ -8,11 +8,14 @@ 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 {
|
||||
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final Logger _logger = getLogger();
|
||||
int _currentTabIndex = 0;
|
||||
|
||||
|
@ -52,17 +55,17 @@ class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> with Singl
|
|||
Container(
|
||||
color: myColor,
|
||||
alignment: Alignment.center,
|
||||
child: UploadView(),
|
||||
child: const UploadView(),
|
||||
),
|
||||
Container(
|
||||
color: myColor,
|
||||
alignment: Alignment.center,
|
||||
child: HistoryView(),
|
||||
child: const HistoryView(),
|
||||
),
|
||||
Container(
|
||||
color: myColor,
|
||||
alignment: Alignment.center,
|
||||
child: ProfileView(),
|
||||
child: const ProfileView(),
|
||||
),
|
||||
][_currentTabIndex],
|
||||
);
|
||||
|
|
|
@ -15,19 +15,22 @@ import 'base_view.dart';
|
|||
class ProfileView extends StatelessWidget {
|
||||
static const routeName = '/profile';
|
||||
|
||||
const ProfileView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseView<ProfileModel>(
|
||||
builder: (context, model, child) =>
|
||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.profile'))), body: _render(model, context)));
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
||||
body: _render(model, context)));
|
||||
}
|
||||
|
||||
Widget _render(ProfileModel model, BuildContext context) {
|
||||
var url = Provider.of<Session>(context).url;
|
||||
var apiKey = Provider.of<Session>(context).apiKey;
|
||||
|
||||
return model.state == ViewState.Busy
|
||||
? Center(child: CircularProgressIndicator())
|
||||
return model.state == ViewState.busy
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView(
|
||||
children: <Widget>[
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
|
@ -45,34 +48,36 @@ class ProfileView extends StatelessWidget {
|
|||
child: Linkify(
|
||||
onOpen: (link) => model.openLink(link.url),
|
||||
text: translate('profile.connection', args: {'url': url}),
|
||||
options: LinkifyOptions(humanize: false),
|
||||
options: const LinkifyOptions(humanize: false),
|
||||
))),
|
||||
UIHelper.verticalSpaceMedium(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||
child: model.configLoading
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
Text(translate('profile.show_config_loading')),
|
||||
],
|
||||
))
|
||||
: ElevatedButton.icon(
|
||||
icon: Icon(Icons.settings, color: blueColor),
|
||||
label: Text(
|
||||
translate('profile.show_config'),
|
||||
),
|
||||
onPressed: () async {
|
||||
await model.showConfig(url);
|
||||
})),
|
||||
child: ElevatedButton.icon(
|
||||
icon: model.configLoading
|
||||
? Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
child: const CircularProgressIndicator(
|
||||
color: blueColor,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
)
|
||||
: 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(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||
child: ElevatedButton.icon(
|
||||
icon: Icon(Icons.lock, color: orangeColor),
|
||||
icon: const Icon(Icons.lock, color: orangeColor),
|
||||
label: Text(
|
||||
translate('profile.reveal_api_key'),
|
||||
),
|
||||
|
@ -83,7 +88,7 @@ class ProfileView extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||
child: ElevatedButton.icon(
|
||||
icon: Icon(Icons.exit_to_app, color: redColor),
|
||||
icon: const Icon(Icons.exit_to_app, color: redColor),
|
||||
label: Text(
|
||||
translate('profile.logout'),
|
||||
),
|
||||
|
|
|
@ -3,26 +3,28 @@ import 'package:stacked/stacked.dart';
|
|||
|
||||
import '../../core/enums/viewstate.dart';
|
||||
import '../../core/viewmodels/startup_model.dart';
|
||||
import '../shared/app_colors.dart';
|
||||
|
||||
class StartUpView extends StatelessWidget {
|
||||
static const routeName = '/';
|
||||
|
||||
const StartUpView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<StartUpViewModel>.reactive(
|
||||
viewModelBuilder: () => StartUpViewModel(),
|
||||
onModelReady: (model) => model.handleStartUpLogic(),
|
||||
onViewModelReady: (model) => model.handleStartUpLogic(),
|
||||
builder: (context, model, child) => Scaffold(
|
||||
backgroundColor: whiteColor,
|
||||
body: model.state == ViewState.Busy
|
||||
body: model.state == ViewState.busy
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
(model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
|
||||
const CircularProgressIndicator(),
|
||||
(model.stateMessage!.isNotEmpty
|
||||
? Text(model.stateMessage!)
|
||||
: Container())
|
||||
]))
|
||||
: Container()));
|
||||
}
|
||||
|
|
|
@ -6,17 +6,18 @@ import 'package:provider/provider.dart';
|
|||
import '../../core/models/session.dart';
|
||||
|
||||
class TabBarContainerView extends StatelessWidget {
|
||||
const TabBarContainerView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Session? currentSession = Provider.of<Session?>(context);
|
||||
bool isAuthenticated = currentSession != null ? currentSession.apiKey.isNotEmpty : false;
|
||||
bool isAuthenticated =
|
||||
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
|
||||
|
||||
if (isAuthenticated) {
|
||||
return AuthenticatedNavBarView();
|
||||
return const AuthenticatedNavBarView();
|
||||
}
|
||||
|
||||
return Container(
|
||||
child: LoginView(),
|
||||
);
|
||||
return LoginView();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:fbmobile/core/util/formatter_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -14,29 +15,35 @@ import 'base_view.dart';
|
|||
class UploadView extends StatelessWidget {
|
||||
static const routeName = '/upload';
|
||||
|
||||
const UploadView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseView<UploadModel>(
|
||||
onModelReady: (model) => model.init(),
|
||||
builder: (context, model, child) =>
|
||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.upload'))), body: _render(model, context)));
|
||||
builder: (context, model, child) => Scaffold(
|
||||
appBar: MyAppBar(title: Text(translate('titles.upload'))),
|
||||
body: _render(model, context)));
|
||||
}
|
||||
|
||||
bool _isUploadButtonEnabled(UploadModel model) {
|
||||
return model.pasteTextTouched || (model.paths != null && model.paths!.length > 0);
|
||||
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
|
||||
return model.state == ViewState.busy
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
(model.stateMessage != null && model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
|
||||
const CircularProgressIndicator(),
|
||||
(model.stateMessage != null && model.stateMessage!.isNotEmpty
|
||||
? Text(model.stateMessage!)
|
||||
: Container())
|
||||
]))
|
||||
: ListView(children: <Widget>[
|
||||
Padding(
|
||||
|
@ -50,16 +57,19 @@ class UploadView extends StatelessWidget {
|
|||
minLines: 1,
|
||||
maxLines: 7,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Icon(
|
||||
prefixIcon: const Icon(
|
||||
Icons.text_snippet,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => model.pasteTextController.clear(),
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () =>
|
||||
model.pasteTextController.clear(),
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
hintText: translate('upload.text_to_be_pasted'),
|
||||
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
||||
contentPadding: const EdgeInsets.fromLTRB(
|
||||
20.0, 10.0, 20.0, 10.0),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(32.0)),
|
||||
),
|
||||
controller: model.pasteTextController)),
|
||||
Padding(
|
||||
|
@ -75,14 +85,17 @@ class UploadView extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.file_copy_sharp, color: blueColor),
|
||||
icon: const Icon(Icons.file_copy_sharp,
|
||||
color: blueColor),
|
||||
onPressed: () => model.openFileExplorer(),
|
||||
label: Text(
|
||||
translate('upload.open_file_explorer'),
|
||||
)),
|
||||
ElevatedButton.icon(
|
||||
icon: Icon(Icons.cancel, color: orangeColor),
|
||||
onPressed: model.paths != null && model.paths!.length > 0
|
||||
icon: const Icon(Icons.cancel,
|
||||
color: orangeColor),
|
||||
onPressed: model.paths != null &&
|
||||
model.paths!.isNotEmpty
|
||||
? () => model.clearCachedFiles()
|
||||
: null,
|
||||
label: Text(
|
||||
|
@ -112,66 +125,107 @@ class UploadView extends StatelessWidget {
|
|||
onPressed: !_isUploadButtonEnabled(model)
|
||||
? null
|
||||
: () async {
|
||||
Map<String, bool>? items = await model.upload();
|
||||
String? clipboardContent = model.generatePasteLinks(items, url);
|
||||
Map<String, bool>? items =
|
||||
await model.upload();
|
||||
String? clipboardContent = model
|
||||
.generatePasteLinks(items, url);
|
||||
|
||||
if (clipboardContent != null && clipboardContent.isNotEmpty) {
|
||||
FlutterClipboard.copy(clipboardContent).then((value) {
|
||||
if (clipboardContent != null &&
|
||||
clipboardContent.isNotEmpty) {
|
||||
FlutterClipboard.copy(
|
||||
clipboardContent)
|
||||
.then((value) {
|
||||
final snackBar = SnackBar(
|
||||
action: SnackBarAction(
|
||||
label: translate('upload.dismiss'),
|
||||
label: translate(
|
||||
'upload.dismiss'),
|
||||
textColor: blueColor,
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(
|
||||
context)
|
||||
.hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
content: Text(translate('upload.uploaded')),
|
||||
duration: Duration(seconds: 10),
|
||||
content: Text(translate(
|
||||
'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: greenColor),
|
||||
icon: const Icon(Icons.upload_rounded,
|
||||
color: greenColor),
|
||||
label: Text(
|
||||
translate('upload.upload'),
|
||||
)),
|
||||
])),
|
||||
model.errorMessage != null && model.errorMessage!.isNotEmpty
|
||||
? (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)))
|
||||
: Container(),
|
||||
Builder(
|
||||
builder: (BuildContext context) => model.loadingPath
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10.0),
|
||||
child: const CircularProgressIndicator(),
|
||||
? const Padding(
|
||||
padding: EdgeInsets.only(bottom: 10.0),
|
||||
child: CircularProgressIndicator(),
|
||||
)
|
||||
: model.paths != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.only(bottom: 30.0),
|
||||
height: MediaQuery.of(context).size.height * 0.50,
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
height:
|
||||
MediaQuery.of(context).size.height * 0.50,
|
||||
child: ListView.separated(
|
||||
itemCount: model.paths != null && model.paths!.isNotEmpty ? model.paths!.length : 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final bool isMultiPath = model.paths != null && model.paths!.isNotEmpty;
|
||||
itemCount: model.paths != null &&
|
||||
model.paths!.isNotEmpty
|
||||
? model.paths!.length
|
||||
: 1,
|
||||
itemBuilder:
|
||||
(BuildContext context, int index) {
|
||||
final bool isMultiPath =
|
||||
model.paths != null &&
|
||||
model.paths!.isNotEmpty;
|
||||
final String name = (isMultiPath
|
||||
? model.paths!.map((e) => e.name).toList()[index]
|
||||
? model.paths!
|
||||
.map((e) => e.name)
|
||||
.toList()[index]
|
||||
: model.fileName ?? '...');
|
||||
final path = model.paths!.length > 0
|
||||
? model.paths!.map((e) => e.path).toList()[index].toString()
|
||||
final size = model.paths!.isNotEmpty
|
||||
? 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(
|
||||
child: ListTile(
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.clear,
|
||||
color: orangeColor),
|
||||
onPressed: () {
|
||||
model.deleteIntentFile(path);
|
||||
}),
|
||||
title: Text(
|
||||
name,
|
||||
"$name (${FormatterUtil.formatBytes(int.parse(size), 2)})",
|
||||
),
|
||||
subtitle: Text(path),
|
||||
));
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) => const Divider(),
|
||||
separatorBuilder:
|
||||
(BuildContext context, int index) =>
|
||||
const Divider(),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
|
|
|
@ -3,18 +3,16 @@ import 'package:flutter/material.dart';
|
|||
import '../../core/services/navigation_service.dart';
|
||||
import '../../locator.dart';
|
||||
import '../../ui/views/about_view.dart';
|
||||
import '../shared/app_colors.dart';
|
||||
|
||||
class AboutIconButton extends StatelessWidget {
|
||||
AboutIconButton();
|
||||
AboutIconButton({super.key});
|
||||
|
||||
final NavigationService _navigationService = locator<NavigationService>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
icon: Icon(Icons.help),
|
||||
color: whiteColor,
|
||||
icon: const Icon(Icons.help),
|
||||
onPressed: () {
|
||||
_navigationService.navigateTo(AboutView.routeName);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ class CenteredErrorRow extends StatelessWidget {
|
|||
final Function? retryCallback;
|
||||
final String? message;
|
||||
|
||||
CenteredErrorRow(this.message, {this.retryCallback});
|
||||
const CenteredErrorRow(this.message, {super.key, this.retryCallback});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -20,7 +20,10 @@ class CenteredErrorRow extends StatelessWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Expanded(child: Center(child: Text(message!, style: TextStyle(color: redColor)))),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(message!,
|
||||
style: const TextStyle(color: redColor)))),
|
||||
],
|
||||
),
|
||||
(retryCallback != null
|
||||
|
@ -30,7 +33,7 @@ class CenteredErrorRow extends StatelessWidget {
|
|||
children: <Widget>[
|
||||
Center(
|
||||
child: IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
icon: const Icon(Icons.refresh),
|
||||
color: primaryAccentColor,
|
||||
onPressed: () {
|
||||
retryCallback!();
|
||||
|
|
|
@ -10,18 +10,25 @@ class LoginApiKeyHeaders extends StatelessWidget {
|
|||
|
||||
final String? validationMessage;
|
||||
|
||||
LoginApiKeyHeaders({required this.uriController, required this.apiKeyController, this.validationMessage});
|
||||
const LoginApiKeyHeaders(
|
||||
{super.key,
|
||||
required this.uriController,
|
||||
required this.apiKeyController,
|
||||
this.validationMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: <Widget>[
|
||||
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
|
||||
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
||||
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'),
|
||||
Icon(Icons.vpn_key),
|
||||
const Icon(Icons.vpn_key),
|
||||
obscureText: true,
|
||||
),
|
||||
]);
|
||||
|
|
|
@ -11,8 +11,9 @@ class LoginCredentialsHeaders extends StatelessWidget {
|
|||
|
||||
final String? validationMessage;
|
||||
|
||||
LoginCredentialsHeaders(
|
||||
{required this.uriController,
|
||||
const LoginCredentialsHeaders(
|
||||
{super.key,
|
||||
required this.uriController,
|
||||
required this.usernameController,
|
||||
required this.passwordController,
|
||||
this.validationMessage});
|
||||
|
@ -20,12 +21,17 @@ class LoginCredentialsHeaders extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(children: <Widget>[
|
||||
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
|
||||
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
||||
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'), Icon(Icons.person),
|
||||
LoginTextField(usernameController,
|
||||
translate('login.username_placeholder'), const Icon(Icons.person),
|
||||
keyboardType: TextInputType.name),
|
||||
LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key),
|
||||
LoginTextField(passwordController,
|
||||
translate('login.password_placeholder'), const Icon(Icons.vpn_key),
|
||||
obscureText: true),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../shared/app_colors.dart';
|
||||
|
||||
class LoginTextField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String placeHolder;
|
||||
|
@ -9,29 +7,31 @@ class LoginTextField extends StatelessWidget {
|
|||
final bool obscureText;
|
||||
final Widget prefixIcon;
|
||||
|
||||
LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
|
||||
{this.keyboardType = TextInputType.text, this.obscureText = false});
|
||||
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: EdgeInsets.symmetric(horizontal: 10.0),
|
||||
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||
height: 50.0,
|
||||
alignment: Alignment.centerLeft,
|
||||
decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)),
|
||||
child: TextFormField(
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
decoration: InputDecoration(
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () => controller.clear(),
|
||||
icon: Icon(Icons.clear),
|
||||
icon: const 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)),
|
||||
contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
||||
border:
|
||||
OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
||||
),
|
||||
controller: controller),
|
||||
);
|
||||
|
|
|
@ -6,13 +6,18 @@ class MyAppBar extends AppBar {
|
|||
static final List<Widget> aboutEnabledWidgets = [AboutIconButton()];
|
||||
static final List<Widget> aboutDisabledWidgets = [];
|
||||
|
||||
MyAppBar({Key? key, required Widget title, List<Widget>? actionWidgets, bool enableAbout = true})
|
||||
: super(key: key, title: Row(children: <Widget>[title]), actions: _renderIconButtons(actionWidgets, enableAbout));
|
||||
MyAppBar(
|
||||
{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) {
|
||||
if (actionWidgets == null) {
|
||||
actionWidgets = [];
|
||||
}
|
||||
static List<Widget> _renderIconButtons(
|
||||
List<Widget>? actionWidgets, bool aboutEnabled) {
|
||||
actionWidgets ??= [];
|
||||
|
||||
List<Widget> widgets = [...actionWidgets];
|
||||
|
||||
|
|
737
pubspec.lock
737
pubspec.lock
File diff suppressed because it is too large
Load diff
48
pubspec.yaml
48
pubspec.yaml
|
@ -11,43 +11,47 @@ description: A mobile client for FileBin.
|
|||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.5.0+16
|
||||
version: 1.6.4+22
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.5 <3.0.0'
|
||||
sdk: '>=3.5.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: 1.0.5
|
||||
cupertino_icons: 1.0.8
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
flutter_translate: 4.0.3
|
||||
provider: 6.0.5
|
||||
stacked: 3.0.1
|
||||
get_it: 7.2.0
|
||||
logger: 1.1.0
|
||||
shared_preferences: 2.0.15
|
||||
http: 0.13.5
|
||||
flutter_translate: 4.1.0
|
||||
provider: 6.1.2
|
||||
stacked: 3.4.3
|
||||
get_it: 7.7.0
|
||||
logger: 2.4.0
|
||||
shared_preferences: 2.3.2
|
||||
http: 1.2.2
|
||||
validators: 3.0.0
|
||||
flutter_linkify: 5.0.2
|
||||
url_launcher: 6.1.7
|
||||
flutter_linkify: 6.0.0
|
||||
url_launcher: 6.3.1
|
||||
expandable: 5.0.1
|
||||
share_plus: 6.3.0
|
||||
file_picker: 5.2.4
|
||||
share_plus: 10.1.1
|
||||
file_picker: 8.1.3
|
||||
clipboard: 0.1.3
|
||||
receive_sharing_intent: 1.4.5
|
||||
permission_handler: 10.2.0
|
||||
package_info_plus: 3.0.2
|
||||
json_annotation: 4.7.0
|
||||
dynamic_color: 1.5.4
|
||||
permission_handler: 11.3.1
|
||||
package_info_plus: 8.1.0
|
||||
json_annotation: 4.9.0
|
||||
dynamic_color: 1.7.0
|
||||
intl: 0.19.0
|
||||
path: 1.9.0
|
||||
flutter_sharing_intent: 1.1.1
|
||||
device_info_plus: 11.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: 2.3.2
|
||||
built_value_generator: 8.4.2
|
||||
json_serializable: 6.5.4
|
||||
build_runner: 2.4.13
|
||||
built_value_generator: 8.9.2
|
||||
json_serializable: 6.8.0
|
||||
flutter_lints: 5.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://www.dartlang.org/tools/pub/pubspec
|
||||
|
|
38
renovate.json5
Normal file
38
renovate.json5
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":rebaseStalePrs",
|
||||
":ignoreUnstable",
|
||||
"group:monorepos",
|
||||
"group:recommended"
|
||||
],
|
||||
"prConcurrentLimit": 0,
|
||||
"schedule": [
|
||||
"monthly"
|
||||
],
|
||||
"ignorePaths": [
|
||||
"android/**",
|
||||
"ios/**"
|
||||
],
|
||||
"ignoreDeps": [
|
||||
"intl",
|
||||
"path"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
"minor"
|
||||
],
|
||||
"groupName": "all minor dependencies",
|
||||
"groupSlug": "all-minor-deps"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": [
|
||||
"patch"
|
||||
],
|
||||
"groupName": "all patch dependencies",
|
||||
"groupSlug": "all-patch-deps"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue