Compare commits
187 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 |
89 changed files with 1707 additions and 878 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
|
# Miscellaneous
|
||||||
*.class
|
*.class
|
||||||
*.lock
|
*.lock
|
||||||
|
!Gemfile.lock
|
||||||
!pubspec.lock
|
!pubspec.lock
|
||||||
*.log
|
*.log
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,5 +1,30 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 1.6.4+22 - 2024/11/01
|
||||||
|
* Dependency updates
|
||||||
|
* Internal build updates
|
||||||
|
|
||||||
|
## 1.6.3+21
|
||||||
|
* Fixed not receiving share requests from other applications
|
||||||
|
|
||||||
|
## 1.6.2+20
|
||||||
|
* Updated internal dependencies
|
||||||
|
* Moved progress indicator of _Show Configuration_ into the underlying button
|
||||||
|
* Bumped Android minSdk to `30` (Android 11)
|
||||||
|
* Bumped Android targetSdk to `34` (Android 14)
|
||||||
|
* Fixed permission service not handling Android SDK 33 correctly
|
||||||
|
* Fixed permission service not being started during application start
|
||||||
|
|
||||||
|
## 1.6.1+19
|
||||||
|
* Updated internal dependencies
|
||||||
|
|
||||||
|
## 1.6.0+18
|
||||||
|
* Fixed input colors in login view when using dark theme
|
||||||
|
* Added removal of individual files selected for upload
|
||||||
|
* Added size for individual files selected for upload
|
||||||
|
* Replaced intent sharing library with `flutter_sharing_intent`
|
||||||
|
* Added proper linting to project
|
||||||
|
|
||||||
## 1.5.1+17
|
## 1.5.1+17
|
||||||
* Fixed white background button in AppBar when light theme enabled
|
* Fixed white background button in AppBar when light theme enabled
|
||||||
* Cleaned up login screen and added icon
|
* Cleaned up login screen and added icon
|
||||||
|
|
|
@ -404,6 +404,14 @@ where to find the applicable terms.
|
||||||
form of a separately written license, or stated as exceptions;
|
form of a separately written license, or stated as exceptions;
|
||||||
the above requirements apply either way.
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
7.1 Exceptions
|
||||||
|
|
||||||
|
7.1.1 As additional permission under section 7, you are allowed to distribute
|
||||||
|
the software through an app store, even if that store has restrictive
|
||||||
|
terms and conditions that are incompatible with the GPL, provided that the
|
||||||
|
source is also available under the GPL with or without this permission through
|
||||||
|
a channel without those restrictive terms and conditions.
|
||||||
|
|
||||||
8. Termination.
|
8. Termination.
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
|
31
README.md
31
README.md
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
A mobile flutter app for [FileBin](https://git.server-speed.net/users/flo/filebin/).
|
A mobile flutter app for [FileBin](https://git.server-speed.net/users/flo/filebin/).
|
||||||
|
|
||||||
Available on the [Play Store](https://play.google.com/store/apps/details?id=de.varakh.fbmobile).
|
Available on the [Play Store](https://play.google.com/store/apps/details?id=de.varakh.fbmobile) and
|
||||||
|
[IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/de.varakh.fbmobile/).
|
||||||
|
|
||||||
The main git repository is hosted at **[https://git.myservermanager.com/varakh/fbmobile](https://git.myservermanager.com/varakh/fbmobile)**.
|
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.
|
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.
|
You need access to the git repository in which those private files reside.
|
||||||
|
|
||||||
#### Usage
|
#### Usage / doing the actual release
|
||||||
|
|
||||||
Go into the platform directory you want to build for, e.g. `ios/` or `android/` and then look into the
|
Go into the platform directory you want to build for, e.g. `ios/` or `android/` and then look into the
|
||||||
`Fastlane` file which lanes are present. Run a lane via `fastlane <platform> <lane>`, e.g. use the
|
`Fastlane` file which lanes are present. Run a lane via `fastlane <platform> <lane>`, e.g. use the
|
||||||
|
@ -104,13 +105,37 @@ following to build for Android `fastlane android build`.
|
||||||
|
|
||||||
##### Android
|
##### Android
|
||||||
|
|
||||||
Use `fastlane android beta` to build and upload a new beta version to the Play Store.
|
It's recommended you set up `fastlane` via `bundler` (you need this to be installed on your machine).
|
||||||
|
|
||||||
|
Go into the `android/` sub-directory of the project
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bundle config set --local path 'vendor/bundle'
|
||||||
|
bundle install
|
||||||
|
|
||||||
|
# update fastlane when needed
|
||||||
|
bundle update fastlane
|
||||||
|
|
||||||
|
# build only
|
||||||
|
bundle exec fastlane android build
|
||||||
|
|
||||||
|
# deploy (push BETA to app store)
|
||||||
|
bundle exec fastlane android beta
|
||||||
|
|
||||||
|
# deploy (push to app store)
|
||||||
|
bundle exec fastlane android deploy
|
||||||
|
|
||||||
|
# deploy (build signed fdroid large bundle [no target and abi split])
|
||||||
|
bundle exec fastlane android build_production_fdroid
|
||||||
|
```
|
||||||
|
|
||||||
##### iOS
|
##### iOS
|
||||||
|
|
||||||
For iOS you need to execute `fastlane ios build` before uploading to testflight with
|
For iOS you need to execute `fastlane ios build` before uploading to testflight with
|
||||||
`fastlane ios beta`.
|
`fastlane ios beta`.
|
||||||
|
|
||||||
|
Probably do the same Ruby/fastlane setup as mentioned under the _Android_ section.
|
||||||
|
|
||||||
### Release manually (not recommended)
|
### Release manually (not recommended)
|
||||||
|
|
||||||
See the following links on how to setup:
|
See the following links on how to setup:
|
||||||
|
|
30
analysis_options.yaml
Normal file
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
|
/gradlew.bat
|
||||||
/local.properties
|
/local.properties
|
||||||
GeneratedPluginRegistrant.java
|
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 localProperties = new Properties()
|
||||||
def localPropertiesFile = rootProject.file('local.properties')
|
def localPropertiesFile = rootProject.file('local.properties')
|
||||||
if (localPropertiesFile.exists()) {
|
if (localPropertiesFile.exists()) {
|
||||||
|
@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
|
||||||
if (flutterRoot == null) {
|
|
||||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
|
||||||
}
|
|
||||||
|
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
if (flutterVersionCode == null) {
|
if (flutterVersionCode == null) {
|
||||||
flutterVersionCode = '1'
|
flutterVersionCode = '1'
|
||||||
|
@ -21,9 +22,6 @@ if (flutterVersionName == null) {
|
||||||
flutterVersionName = '1.0'
|
flutterVersionName = '1.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
|
||||||
|
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||||
if (keystorePropertiesFile.exists()) {
|
if (keystorePropertiesFile.exists()) {
|
||||||
|
@ -31,7 +29,9 @@ if (keystorePropertiesFile.exists()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
|
|
||||||
|
namespace "de.varakh.fbmobile"
|
||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
disable 'InvalidPackage'
|
disable 'InvalidPackage'
|
||||||
|
@ -39,8 +39,8 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "de.varakh.fbmobile"
|
applicationId "de.varakh.fbmobile"
|
||||||
minSdkVersion 16
|
minSdkVersion 30
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
|
|
@ -21,36 +21,31 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
@ -70,7 +65,10 @@
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
to allow setting breakpoints, to provide hot reload, etc.
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
-->
|
-->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||||
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
|
|
@ -1,14 +1,3 @@
|
||||||
buildscript {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
|
@ -24,6 +13,6 @@ subprojects {
|
||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,11 @@ platform :android do
|
||||||
sh("#{ENV['PWD']}/fastlane/buildAndroidProduction.sh")
|
sh("#{ENV['PWD']}/fastlane/buildAndroidProduction.sh")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Build Production fdroid"
|
||||||
|
lane :build_production_fdroid do
|
||||||
|
sh("#{ENV['PWD']}/fastlane/buildAndroidProductionFdroid.sh")
|
||||||
|
end
|
||||||
|
|
||||||
desc "Build"
|
desc "Build"
|
||||||
lane :build do
|
lane :build do
|
||||||
sh("#{ENV['PWD']}/fastlane/buildAndroid.sh")
|
sh("#{ENV['PWD']}/fastlane/buildAndroid.sh")
|
||||||
|
|
|
@ -31,6 +31,14 @@ Build Debug
|
||||||
|
|
||||||
Build Production
|
Build Production
|
||||||
|
|
||||||
|
### android build_production_fdroid
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android build_production_fdroid
|
||||||
|
```
|
||||||
|
|
||||||
|
Build Production fdroid
|
||||||
|
|
||||||
### android build
|
### android build
|
||||||
|
|
||||||
```sh
|
```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
|
org.gradle.jvmargs=-Xmx1536M
|
||||||
android.enableR8=true
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#Fri Jun 23 08:50:38 CEST 2017
|
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-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()
|
repositories {
|
||||||
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
|
google()
|
||||||
if (pluginsFile.exists()) {
|
mavenCentral()
|
||||||
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins.each { name, path ->
|
plugins {
|
||||||
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
include ":$name"
|
id "com.android.application" version "${agpVersion}" apply false
|
||||||
project(":$name").projectDir = pluginDirectory
|
id "org.jetbrains.kotlin.android" version "${kotlinVersion}" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include ":app"
|
||||||
|
|
|
@ -126,14 +126,6 @@
|
||||||
"description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types."
|
"description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"permission_service": {
|
|
||||||
"dialog": {
|
|
||||||
"title": "Storage permission",
|
|
||||||
"description": "Storage permission should be granted to the app so that it can work properly. Do you want to grant permission or ignore this message permanently in the future?",
|
|
||||||
"grant": "Grant",
|
|
||||||
"ignore": "Ignore"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"confirm": "OK",
|
"confirm": "OK",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
</dict>
|
</dict>
|
||||||
<dict/>
|
<dict/>
|
||||||
</array>
|
</array>
|
||||||
// TODO follow steps 2) on create share extension (https://pub.dev/packages/receive_sharing_intent)
|
// TODO follow steps on create share extension (https://pub.dev/packages/flutter_sharing_intent)
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Allow to select photos and upload them via the app</string>
|
<string>Allow to select photos and upload them via the app</string>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<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';
|
import 'ui/views/startup_view.dart';
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class MyApp extends StatelessWidget {
|
||||||
static final _defaultLightColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.light);
|
static final _defaultLightColorScheme = ColorScheme.fromSwatch(
|
||||||
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.dark);
|
primarySwatch: myColor, brightness: Brightness.light);
|
||||||
|
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
|
||||||
|
primarySwatch: myColor, brightness: Brightness.dark);
|
||||||
|
|
||||||
MyApp() {
|
MyApp({super.key}) {
|
||||||
initializeDateFormatting('en');
|
initializeDateFormatting('en');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,26 +35,33 @@ class MyApp extends StatelessWidget {
|
||||||
state: LocalizationProvider.of(context).state,
|
state: LocalizationProvider.of(context).state,
|
||||||
child: StreamProvider<RefreshEvent?>(
|
child: StreamProvider<RefreshEvent?>(
|
||||||
initialData: null,
|
initialData: null,
|
||||||
create: (context) => locator<RefreshService>().refreshEventController.stream,
|
create: (context) =>
|
||||||
|
locator<RefreshService>().refreshEventController.stream,
|
||||||
child: StreamProvider<Session?>(
|
child: StreamProvider<Session?>(
|
||||||
initialData: Session.initial(),
|
initialData: Session.initial(),
|
||||||
create: (context) => locator<SessionService>().sessionController.stream,
|
create: (context) =>
|
||||||
child: LifeCycleManager(child: DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
|
locator<SessionService>().sessionController.stream,
|
||||||
|
child: LifeCycleManager(child: DynamicColorBuilder(
|
||||||
|
builder: (lightColorScheme, darkColorScheme) {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
title: translate('app.title'),
|
title: translate('app.title'),
|
||||||
builder: (context, child) => Navigator(
|
builder: (context, child) => Navigator(
|
||||||
key: locator<DialogService>().dialogNavigationKey,
|
key: locator<DialogService>().dialogNavigationKey,
|
||||||
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
|
onGenerateRoute: (settings) => MaterialPageRoute(
|
||||||
|
builder: (context) => DialogManager(child: child)),
|
||||||
),
|
),
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
colorScheme: lightColorScheme ?? _defaultLightColorScheme),
|
colorScheme:
|
||||||
darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
|
lightColorScheme ?? _defaultLightColorScheme),
|
||||||
|
darkTheme: ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
|
||||||
onGenerateRoute: AppRouter.generateRoute,
|
onGenerateRoute: AppRouter.generateRoute,
|
||||||
navigatorKey: locator<NavigationService>().navigationKey,
|
navigatorKey: locator<NavigationService>().navigationKey,
|
||||||
home: StartUpView(),
|
home: const StartUpView(),
|
||||||
supportedLocales: localizationDelegate.supportedLocales,
|
supportedLocales: localizationDelegate.supportedLocales,
|
||||||
locale: localizationDelegate.currentLocale,
|
locale: localizationDelegate.currentLocale,
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
/// Enums for error codes
|
/// Enums for error codes
|
||||||
enum ErrorCode {
|
enum ErrorCode {
|
||||||
/// A generic error
|
/// A generic error
|
||||||
GENERAL_ERROR,
|
generalError,
|
||||||
|
|
||||||
/// Errors related to connections
|
/// Errors related to connections
|
||||||
SOCKET_ERROR,
|
socketError,
|
||||||
SOCKET_TIMEOUT,
|
socketTimeout,
|
||||||
|
|
||||||
/// A REST error (response code wasn't 200 or 204)
|
/// A REST error (response code wasn't 200 or 204)
|
||||||
REST_ERROR,
|
restError,
|
||||||
|
|
||||||
/// Custom errors
|
/// 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 int statusCode;
|
||||||
final dynamic responseBody;
|
final dynamic responseBody;
|
||||||
|
|
||||||
RestServiceException(this.statusCode, {this.responseBody, String? message})
|
RestServiceException(this.statusCode, {this.responseBody, super.message = null})
|
||||||
: super(code: ErrorCode.REST_ERROR, message: message);
|
: super(code: ErrorCode.restError);
|
||||||
|
|
||||||
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "$code $statusCode $message";
|
return "$code $statusCode $message";
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ class ServiceException implements Exception {
|
||||||
final ErrorCode code;
|
final ErrorCode code;
|
||||||
final String? message;
|
final String? message;
|
||||||
|
|
||||||
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''});
|
ServiceException({this.code = ErrorCode.generalError, this.message = ''});
|
||||||
|
|
||||||
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return "$code: $message";
|
return "$code: $message";
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,14 @@ import '../services/dialog_service.dart';
|
||||||
class DialogManager extends StatefulWidget {
|
class DialogManager extends StatefulWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
DialogManager({Key? key, this.child}) : super(key: key);
|
const DialogManager({super.key, this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
_DialogManagerState createState() => _DialogManagerState();
|
_DialogManagerState createState() => _DialogManagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DialogManagerState extends State<DialogManager> {
|
class _DialogManagerState extends State<DialogManager> {
|
||||||
DialogService _dialogService = locator<DialogService>();
|
final DialogService _dialogService = locator<DialogService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -30,7 +31,8 @@ class _DialogManagerState extends State<DialogManager> {
|
||||||
void _showDialog(DialogRequest request) {
|
void _showDialog(DialogRequest request) {
|
||||||
List<Widget> actions = <Widget>[];
|
List<Widget> actions = <Widget>[];
|
||||||
|
|
||||||
if (request.buttonTitleDeny != null && request.buttonTitleDeny!.isNotEmpty) {
|
if (request.buttonTitleDeny != null &&
|
||||||
|
request.buttonTitleDeny!.isNotEmpty) {
|
||||||
Widget denyBtn = TextButton(
|
Widget denyBtn = TextButton(
|
||||||
child: Text(request.buttonTitleDeny!),
|
child: Text(request.buttonTitleDeny!),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
|
|
@ -11,15 +11,20 @@ import '../util/logger.dart';
|
||||||
class LifeCycleManager extends StatefulWidget {
|
class LifeCycleManager extends StatefulWidget {
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
LifeCycleManager({Key? key, this.child}) : super(key: key);
|
const LifeCycleManager({super.key, this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
_LifeCycleManagerState createState() => _LifeCycleManagerState();
|
_LifeCycleManagerState createState() => _LifeCycleManagerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBindingObserver {
|
class _LifeCycleManagerState extends State<LifeCycleManager>
|
||||||
|
with WidgetsBindingObserver {
|
||||||
final Logger logger = getLogger();
|
final Logger logger = getLogger();
|
||||||
|
|
||||||
List<StoppableService> servicesToManage = [locator<SessionService>(), locator<PermissionService>()];
|
List<StoppableService> servicesToManage = [
|
||||||
|
locator<SessionService>(),
|
||||||
|
locator<PermissionService>()
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -43,12 +48,12 @@ class _LifeCycleManagerState extends State<LifeCycleManager> with WidgetsBinding
|
||||||
logger.d('LifeCycle event ${state.toString()}');
|
logger.d('LifeCycle event ${state.toString()}');
|
||||||
super.didChangeAppLifecycleState(state);
|
super.didChangeAppLifecycleState(state);
|
||||||
|
|
||||||
servicesToManage.forEach((service) {
|
for (var service in servicesToManage) {
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
service.start();
|
service.start();
|
||||||
} else {
|
} else {
|
||||||
service.stop();
|
service.stop();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,11 @@ class ApiKey {
|
||||||
|
|
||||||
final String? comment;
|
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
|
// JSON Init
|
||||||
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);
|
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);
|
||||||
|
|
|
@ -12,7 +12,8 @@ class ApiKeys {
|
||||||
ApiKeys({required this.apikeys});
|
ApiKeys({required this.apikeys});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
|
factory ApiKeys.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ApiKeysFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);
|
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class ApiKeysResponse {
|
||||||
ApiKeysResponse({required this.status, required this.data});
|
ApiKeysResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
|
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ApiKeysResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);
|
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class ConfigResponse {
|
||||||
ConfigResponse({required this.status, required this.data});
|
ConfigResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json);
|
factory ConfigResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ConfigResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);
|
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);
|
||||||
|
|
|
@ -13,7 +13,8 @@ class CreateApiKeyResponse {
|
||||||
CreateApiKeyResponse({required this.status, required this.data});
|
CreateApiKeyResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
|
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CreateApiKeyResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);
|
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);
|
||||||
|
|
|
@ -19,7 +19,8 @@ class History {
|
||||||
History({required this.items, required this.multipasteItems, this.totalSize});
|
History({required this.items, required this.multipasteItems, this.totalSize});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);
|
factory History.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$HistoryFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$HistoryToJson(this);
|
Map<String, dynamic> toJson() => _$HistoryToJson(this);
|
||||||
|
|
|
@ -23,7 +23,8 @@ class HistoryItem {
|
||||||
this.thumbnail});
|
this.thumbnail});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json);
|
factory HistoryItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$HistoryItemFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$HistoryItemToJson(this);
|
Map<String, dynamic> toJson() => _$HistoryItemToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class HistoryMultipasteItem {
|
||||||
HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
|
HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json);
|
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$HistoryMultipasteItemFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);
|
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);
|
||||||
|
|
|
@ -9,7 +9,8 @@ class HistoryMultipasteItemEntry {
|
||||||
HistoryMultipasteItemEntry({required this.id});
|
HistoryMultipasteItemEntry({required this.id});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemEntryFromJson(json);
|
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$HistoryMultipasteItemEntryFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);
|
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class HistoryResponse {
|
||||||
HistoryResponse({required this.status, required this.data});
|
HistoryResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json);
|
factory HistoryResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$HistoryResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);
|
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class RestError {
|
||||||
required this.errorId,
|
required this.errorId,
|
||||||
}); // JSON Init
|
}); // JSON Init
|
||||||
|
|
||||||
factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json);
|
factory RestError.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$RestErrorFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$RestErrorToJson(this);
|
Map<String, dynamic> toJson() => _$RestErrorToJson(this);
|
||||||
|
|
|
@ -13,7 +13,8 @@ class Uploaded {
|
||||||
Uploaded({required this.ids, required this.urls});
|
Uploaded({required this.ids, required this.urls});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json);
|
factory Uploaded.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadedFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$UploadedToJson(this);
|
Map<String, dynamic> toJson() => _$UploadedToJson(this);
|
||||||
|
|
|
@ -13,7 +13,8 @@ class UploadedMulti {
|
||||||
UploadedMulti({required this.url, required this.urlId});
|
UploadedMulti({required this.url, required this.urlId});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json);
|
factory UploadedMulti.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadedMultiFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);
|
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class UploadedMultiResponse {
|
||||||
UploadedMultiResponse({required this.status, required this.data});
|
UploadedMultiResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json);
|
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadedMultiResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);
|
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);
|
||||||
|
|
|
@ -15,7 +15,8 @@ class UploadedResponse {
|
||||||
UploadedResponse({required this.status, required this.data});
|
UploadedResponse({required this.status, required this.data});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json);
|
factory UploadedResponse.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadedResponseFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);
|
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);
|
||||||
|
|
|
@ -13,7 +13,8 @@ class Session {
|
||||||
: url = '',
|
: url = '',
|
||||||
apiKey = '';
|
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);
|
Map<String, dynamic> toJson() => _$SessionToJson(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ class UploadedPaste {
|
||||||
this.items});
|
this.items});
|
||||||
|
|
||||||
// JSON Init
|
// JSON Init
|
||||||
factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json);
|
factory UploadedPaste.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$UploadedPasteFromJson(json);
|
||||||
|
|
||||||
// JSON Export
|
// JSON Export
|
||||||
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);
|
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);
|
||||||
|
|
|
@ -11,7 +11,7 @@ import '../models/rest/uploaded_response.dart';
|
||||||
import '../services/api.dart';
|
import '../services/api.dart';
|
||||||
|
|
||||||
class FileRepository {
|
class FileRepository {
|
||||||
Api _api = locator<Api>();
|
final Api _api = locator<Api>();
|
||||||
|
|
||||||
Future<History> getHistory() async {
|
Future<History> getHistory() async {
|
||||||
var response = await _api.post('/file/history');
|
var response = await _api.post('/file/history');
|
||||||
|
@ -33,19 +33,23 @@ class FileRepository {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UploadedResponse> postUpload(List<File>? files, Map<String, String>? additionalFiles) async {
|
Future<UploadedResponse> postUpload(
|
||||||
var response = await _api.post('/file/upload', files: files, additionalFiles: additionalFiles);
|
List<File>? files, Map<String, String>? additionalFiles) async {
|
||||||
|
var response = await _api.post('/file/upload',
|
||||||
|
files: files, additionalFiles: additionalFiles);
|
||||||
return UploadedResponse.fromJson(json.decode(response.body));
|
return UploadedResponse.fromJson(json.decode(response.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UploadedMultiResponse> postCreateMultiPaste(List<String> ids) async {
|
Future<UploadedMultiResponse> postCreateMultiPaste(List<String> ids) async {
|
||||||
Map<String, String> multiPasteIds = Map();
|
Map<String, String> multiPasteIds = {};
|
||||||
|
|
||||||
ids.forEach((element) {
|
for (var element in ids) {
|
||||||
multiPasteIds.putIfAbsent("ids[${ids.indexOf(element) + 1}]", () => element);
|
multiPasteIds.putIfAbsent(
|
||||||
});
|
"ids[${ids.indexOf(element) + 1}]", () => element);
|
||||||
|
}
|
||||||
|
|
||||||
var response = await _api.post('/file/create_multipaste', fields: multiPasteIds);
|
var response =
|
||||||
|
await _api.post('/file/create_multipaste', fields: multiPasteIds);
|
||||||
return UploadedMultiResponse.fromJson(json.decode(response.body));
|
return UploadedMultiResponse.fromJson(json.decode(response.body));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,10 @@ import '../models/rest/create_apikey_response.dart';
|
||||||
import '../services/api.dart';
|
import '../services/api.dart';
|
||||||
|
|
||||||
class UserRepository {
|
class UserRepository {
|
||||||
Api _api = locator<Api>();
|
final Api _api = locator<Api>();
|
||||||
|
|
||||||
Future<CreateApiKeyResponse> postApiKey(
|
Future<CreateApiKeyResponse> postApiKey(String url, String username,
|
||||||
String url, String username, String password, String accessLevel, String comment) async {
|
String password, String accessLevel, String comment) async {
|
||||||
_api.setUrl(url);
|
_api.setUrl(url);
|
||||||
|
|
||||||
var fields = Map.fromEntries([
|
var fields = Map.fromEntries([
|
||||||
|
|
|
@ -24,25 +24,34 @@ class Api implements ApiErrorConverter {
|
||||||
String _url = "";
|
String _url = "";
|
||||||
String _apiKey = "";
|
String _apiKey = "";
|
||||||
|
|
||||||
Map<String, String> _headers = {"Content-Type": _applicationJson, "Accept": _applicationJson};
|
final Map<String, String> _headers = {
|
||||||
Duration _timeout = Duration(seconds: Constants.apiRequestTimeoutLimit);
|
"Content-Type": _applicationJson,
|
||||||
|
"Accept": _applicationJson
|
||||||
|
};
|
||||||
|
Duration _timeout = const Duration(seconds: Constants.apiRequestTimeoutLimit);
|
||||||
|
|
||||||
Future<http.Response> fetch<T>(String route) async {
|
Future<http.Response> fetch<T>(String route) async {
|
||||||
try {
|
try {
|
||||||
_logger
|
_logger.d(
|
||||||
.d("Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
|
"Requesting GET API endpoint '${_url + route}' with headers '$_headers' and maximum timeout '$_timeout'");
|
||||||
var response = await http.get(Uri.parse(_url + route), headers: _headers).timeout(_timeout);
|
var response = await http
|
||||||
|
.get(Uri.parse(_url + route), headers: _headers)
|
||||||
|
.timeout(_timeout);
|
||||||
handleRestErrors(response);
|
handleRestErrors(response);
|
||||||
return response;
|
return response;
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
|
throw ServiceException(
|
||||||
|
code: ErrorCode.socketTimeout, message: _errorTimeout);
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
|
throw ServiceException(
|
||||||
|
code: ErrorCode.socketError, message: _errorNoConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<http.Response> post<T>(String route,
|
Future<http.Response> post<T>(String route,
|
||||||
{Map<String, String?>? fields, List<File>? files, Map<String, String>? additionalFiles}) async {
|
{Map<String, String?>? fields,
|
||||||
|
List<File>? files,
|
||||||
|
Map<String, String>? additionalFiles}) async {
|
||||||
try {
|
try {
|
||||||
var uri = Uri.parse(_url + route);
|
var uri = Uri.parse(_url + route);
|
||||||
var request = http.MultipartRequest('POST', uri)
|
var request = http.MultipartRequest('POST', uri)
|
||||||
|
@ -58,28 +67,35 @@ class Api implements ApiErrorConverter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files != null && files.isNotEmpty) {
|
if (files != null && files.isNotEmpty) {
|
||||||
files.forEach((element) async {
|
for (var element in files) {
|
||||||
request.files.add(await http.MultipartFile.fromPath('file[${files.indexOf(element) + 1}]', element.path));
|
request.files.add(await http.MultipartFile.fromPath(
|
||||||
});
|
'file[${files.indexOf(element) + 1}]', element.path));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalFiles != null && additionalFiles.length > 0) {
|
if (additionalFiles != null && additionalFiles.isNotEmpty) {
|
||||||
List<String> keys = additionalFiles.keys.toList();
|
List<String> keys = additionalFiles.keys.toList();
|
||||||
additionalFiles.forEach((key, value) {
|
additionalFiles.forEach((key, value) {
|
||||||
var index = files != null ? files.length + keys.indexOf(key) + 1 : keys.indexOf(key) + 1;
|
var index = files != null
|
||||||
request.files.add(http.MultipartFile.fromString('file[$index]', value, filename: key));
|
? files.length + keys.indexOf(key) + 1
|
||||||
|
: keys.indexOf(key) + 1;
|
||||||
|
request.files.add(http.MultipartFile.fromString('file[$index]', value,
|
||||||
|
filename: key));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.d("Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
|
_logger.d(
|
||||||
|
"Requesting POST API endpoint '${uri.toString()}' and ${request.files.length} files");
|
||||||
var multiResponse = await request.send();
|
var multiResponse = await request.send();
|
||||||
var response = await http.Response.fromStream(multiResponse);
|
var response = await http.Response.fromStream(multiResponse);
|
||||||
handleRestErrors(response);
|
handleRestErrors(response);
|
||||||
return response;
|
return response;
|
||||||
} on TimeoutException {
|
} on TimeoutException {
|
||||||
throw ServiceException(code: ErrorCode.SOCKET_TIMEOUT, message: _errorTimeout);
|
throw ServiceException(
|
||||||
|
code: ErrorCode.socketTimeout, message: _errorTimeout);
|
||||||
} on SocketException {
|
} on SocketException {
|
||||||
throw ServiceException(code: ErrorCode.SOCKET_ERROR, message: _errorNoConnection);
|
throw ServiceException(
|
||||||
|
code: ErrorCode.socketError, message: _errorNoConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,18 +123,21 @@ class Api implements ApiErrorConverter {
|
||||||
/// have a json decoded object. Replace this with a custom
|
/// have a json decoded object. Replace this with a custom
|
||||||
/// conversion method by overwriting the interface if needed
|
/// conversion method by overwriting the interface if needed
|
||||||
void handleRestErrors(http.Response response) {
|
void handleRestErrors(http.Response response) {
|
||||||
if (response.statusCode != HttpStatus.ok && response.statusCode != HttpStatus.noContent) {
|
if (response.statusCode != HttpStatus.ok &&
|
||||||
|
response.statusCode != HttpStatus.noContent) {
|
||||||
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
|
if (response.headers.containsKey(HttpHeaders.contentTypeHeader)) {
|
||||||
ContentType responseContentType = ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
|
ContentType responseContentType =
|
||||||
|
ContentType.parse(response.headers[HttpHeaders.contentTypeHeader]!);
|
||||||
|
|
||||||
if (ContentType.json.primaryType == responseContentType.primaryType &&
|
if (ContentType.json.primaryType == responseContentType.primaryType &&
|
||||||
ContentType.json.subType == responseContentType.subType) {
|
ContentType.json.subType == responseContentType.subType) {
|
||||||
var parsedBody = convert(response);
|
var parsedBody = convert(response);
|
||||||
throw new RestServiceException(response.statusCode, responseBody: parsedBody);
|
throw RestServiceException(response.statusCode,
|
||||||
|
responseBody: parsedBody);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RestServiceException(response.statusCode);
|
throw RestServiceException(response.statusCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,8 @@ import '../datamodels/dialog_request.dart';
|
||||||
import '../datamodels/dialog_response.dart';
|
import '../datamodels/dialog_response.dart';
|
||||||
|
|
||||||
class DialogService {
|
class DialogService {
|
||||||
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> _dialogNavigationKey =
|
||||||
|
GlobalKey<NavigatorState>();
|
||||||
late Function(DialogRequest) _showDialogListener;
|
late Function(DialogRequest) _showDialogListener;
|
||||||
Completer<DialogResponse>? _dialogCompleter;
|
Completer<DialogResponse>? _dialogCompleter;
|
||||||
|
|
||||||
|
@ -27,20 +28,28 @@ class DialogService {
|
||||||
title: title,
|
title: title,
|
||||||
description: description,
|
description: description,
|
||||||
buttonTitleAccept:
|
buttonTitleAccept:
|
||||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept));
|
buttonTitleAccept == null || buttonTitleAccept.isEmpty
|
||||||
|
? translate('dialog.confirm')
|
||||||
|
: buttonTitleAccept));
|
||||||
return _dialogCompleter!.future;
|
return _dialogCompleter!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DialogResponse> showConfirmationDialog(
|
Future<DialogResponse> showConfirmationDialog(
|
||||||
{String? title, String? description, String? buttonTitleAccept, String? buttonTitleDeny}) {
|
{String? title,
|
||||||
|
String? description,
|
||||||
|
String? buttonTitleAccept,
|
||||||
|
String? buttonTitleDeny}) {
|
||||||
_dialogCompleter = Completer<DialogResponse>();
|
_dialogCompleter = Completer<DialogResponse>();
|
||||||
_showDialogListener(DialogRequest(
|
_showDialogListener(DialogRequest(
|
||||||
title: title,
|
title: title,
|
||||||
description: description,
|
description: description,
|
||||||
buttonTitleAccept:
|
buttonTitleAccept:
|
||||||
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept,
|
buttonTitleAccept == null || buttonTitleAccept.isEmpty
|
||||||
buttonTitleDeny:
|
? translate('dialog.confirm')
|
||||||
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny));
|
: buttonTitleAccept,
|
||||||
|
buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
|
||||||
|
? translate('dialog.cancel')
|
||||||
|
: buttonTitleDeny));
|
||||||
return _dialogCompleter!.future;
|
return _dialogCompleter!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,8 @@ class FileService {
|
||||||
return await _fileRepository.postDelete(id);
|
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);
|
return await _fileRepository.postUpload(files, additionalFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,8 @@ class LinkService {
|
||||||
_logger.e('Could not launch link $link');
|
_logger.e('Could not launch link $link');
|
||||||
_dialogService.showDialog(
|
_dialogService.showDialog(
|
||||||
title: translate('link.dialog.title'),
|
title: translate('link.dialog.title'),
|
||||||
description: translate('link.dialog.description', args: {'link': link}));
|
description:
|
||||||
|
translate('link.dialog.description', args: {'link': link}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'package:logger/logger.dart';
|
||||||
import '../util/logger.dart';
|
import '../util/logger.dart';
|
||||||
|
|
||||||
class NavigationService {
|
class NavigationService {
|
||||||
GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> _navigationKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
GlobalKey<NavigatorState> get navigationKey => _navigationKey;
|
GlobalKey<NavigatorState> get navigationKey => _navigationKey;
|
||||||
|
|
||||||
|
@ -17,11 +17,13 @@ class NavigationService {
|
||||||
|
|
||||||
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
|
Future<dynamic> navigateTo(String routeName, {dynamic arguments}) {
|
||||||
logger.d('NavigationService: navigateTo $routeName');
|
logger.d('NavigationService: navigateTo $routeName');
|
||||||
return _navigationKey.currentState!.pushNamed(routeName, arguments: arguments);
|
return _navigationKey.currentState!
|
||||||
|
.pushNamed(routeName, arguments: arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) {
|
Future<dynamic> navigateAndReplaceTo(String routeName, {dynamic arguments}) {
|
||||||
logger.d('NavigationService: navigateAndReplaceTo $routeName');
|
logger.d('NavigationService: navigateAndReplaceTo $routeName');
|
||||||
return _navigationKey.currentState!.pushReplacementNamed(routeName, arguments: arguments);
|
return _navigationKey.currentState!
|
||||||
|
.pushReplacementNamed(routeName, arguments: arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,115 +1,109 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import '../../constants.dart';
|
import '../../constants.dart';
|
||||||
import '../../core/datamodels/dialog_response.dart';
|
|
||||||
import '../../core/services/dialog_service.dart';
|
|
||||||
import '../../core/services/stoppable_service.dart';
|
import '../../core/services/stoppable_service.dart';
|
||||||
import '../../core/util/logger.dart';
|
import '../../core/util/logger.dart';
|
||||||
import '../../locator.dart';
|
|
||||||
import 'storage_service.dart';
|
|
||||||
|
|
||||||
class PermissionService extends StoppableService {
|
class PermissionService extends StoppableService {
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
final DialogService _dialogService = locator<DialogService>();
|
|
||||||
final StorageService _storageService = locator<StorageService>();
|
|
||||||
|
|
||||||
Timer? _serviceCheckTimer;
|
Timer? _serviceCheckTimer;
|
||||||
|
|
||||||
PermissionStatus? _permissionStatus;
|
|
||||||
|
|
||||||
bool _permanentlyIgnored = false;
|
|
||||||
bool _devicePermissionDialogActive = false;
|
bool _devicePermissionDialogActive = false;
|
||||||
bool _ownPermissionDialogActive = false;
|
|
||||||
|
|
||||||
PermissionService() {
|
bool _deviceInformationInitialized = false;
|
||||||
_devicePermissionDialogActive = true;
|
bool _useStoragePermission = true;
|
||||||
|
|
||||||
Permission.storage.request().then((status) {
|
PermissionService();
|
||||||
_permissionStatus = status;
|
|
||||||
if (PermissionStatus.permanentlyDenied == status) {
|
|
||||||
_permanentlyIgnored = true;
|
|
||||||
}
|
|
||||||
}).whenComplete(() {
|
|
||||||
_logger.d('Initial device request permission finished');
|
|
||||||
_devicePermissionDialogActive = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future checkEnabledAndPermission() async {
|
Future checkEnabledAndPermission() async {
|
||||||
if (_permanentlyIgnored) {
|
|
||||||
await _storageService.storeStoragePermissionDialogIgnored();
|
|
||||||
_permanentlyIgnored = false;
|
|
||||||
_logger.d('Set permanently ignored permission request');
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_devicePermissionDialogActive) {
|
if (_devicePermissionDialogActive) {
|
||||||
_logger.d('Device permission dialog active, skipping');
|
_logger.d('Device permission dialog active, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_ownPermissionDialogActive) {
|
bool allGranted = false;
|
||||||
_logger.d('Own permission dialog already active, skipping');
|
bool anyPermanentlyDenied = false;
|
||||||
return;
|
|
||||||
|
// Since Android compileSdk >= 33, "storage" is deprecated
|
||||||
|
// Instead, request access to all of
|
||||||
|
// - Permission.photos
|
||||||
|
// - Permission.videos
|
||||||
|
// - Permission.audio
|
||||||
|
//
|
||||||
|
// For iOS and Android < 33, keep using "storage"
|
||||||
|
if (_useStoragePermission) {
|
||||||
|
PermissionStatus storagePermission = await Permission.storage.status;
|
||||||
|
allGranted = PermissionStatus.granted == storagePermission;
|
||||||
|
anyPermanentlyDenied =
|
||||||
|
PermissionStatus.permanentlyDenied == storagePermission;
|
||||||
|
} else {
|
||||||
|
PermissionStatus photosPermission = await Permission.photos.status;
|
||||||
|
PermissionStatus videosPermission = await Permission.videos.status;
|
||||||
|
PermissionStatus audioPermission = await Permission.audio.status;
|
||||||
|
|
||||||
|
allGranted = PermissionStatus.granted == photosPermission &&
|
||||||
|
PermissionStatus.granted == videosPermission &&
|
||||||
|
PermissionStatus.granted == audioPermission;
|
||||||
|
anyPermanentlyDenied =
|
||||||
|
PermissionStatus.permanentlyDenied == photosPermission ||
|
||||||
|
PermissionStatus.permanentlyDenied == videosPermission ||
|
||||||
|
PermissionStatus.permanentlyDenied == audioPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ignoredDialog = await _storageService.hasStoragePermissionDialogIgnored();
|
// show warning to user to manually handle, don't enforce it over and over again
|
||||||
|
if (anyPermanentlyDenied) {
|
||||||
if (ignoredDialog) {
|
_logger.w(
|
||||||
_logger.d('Permanently ignored permission request, skipping');
|
"At least one required permission has been denied permanently, stopping service");
|
||||||
stop();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_permissionStatus = await Permission.storage.status;
|
// all good, stop the permission service
|
||||||
if (_permissionStatus != PermissionStatus.granted) {
|
if (allGranted) {
|
||||||
if (_permissionStatus == PermissionStatus.permanentlyDenied) {
|
_logger.d("All permissions have been granted, stopping service");
|
||||||
await _storageService.storeStoragePermissionDialogIgnored();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ownPermissionDialogActive = true;
|
// not all have been granted, show OS dialog
|
||||||
DialogResponse response = await _dialogService.showConfirmationDialog(
|
_logger.d(
|
||||||
title: translate('permission_service.dialog.title'),
|
"Not all permissions have been granted yet, initializing permission dialog");
|
||||||
description: translate('permission_service.dialog.description'),
|
|
||||||
buttonTitleAccept: translate('permission_service.dialog.grant'),
|
|
||||||
buttonTitleDeny: translate('permission_service.dialog.ignore'));
|
|
||||||
|
|
||||||
if (!response.confirmed!) {
|
|
||||||
await _storageService.storeStoragePermissionDialogIgnored();
|
|
||||||
} else {
|
|
||||||
_devicePermissionDialogActive = true;
|
_devicePermissionDialogActive = true;
|
||||||
Permission.storage.request().then((status) async {
|
|
||||||
if (PermissionStatus.permanentlyDenied == status) {
|
if (_useStoragePermission) {
|
||||||
await _storageService.storeStoragePermissionDialogIgnored();
|
await [Permission.storage].request().whenComplete(() {
|
||||||
}
|
|
||||||
}).whenComplete(() {
|
|
||||||
_logger.d('Device request permission finished');
|
_logger.d('Device request permission finished');
|
||||||
_devicePermissionDialogActive = false;
|
_devicePermissionDialogActive = false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
_ownPermissionDialogActive = false;
|
|
||||||
} else {
|
} else {
|
||||||
await _storageService.storeStoragePermissionDialogIgnored();
|
await [Permission.photos, Permission.videos, Permission.audio]
|
||||||
|
.request()
|
||||||
|
.whenComplete(() {
|
||||||
|
_logger.d('Device request permission finished');
|
||||||
|
_devicePermissionDialogActive = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future start() async {
|
Future start() async {
|
||||||
super.start();
|
super.start();
|
||||||
|
await _determineDeviceInfo();
|
||||||
await checkEnabledAndPermission();
|
await checkEnabledAndPermission();
|
||||||
|
|
||||||
_serviceCheckTimer =
|
_serviceCheckTimer = Timer.periodic(
|
||||||
Timer.periodic(Duration(milliseconds: Constants.mediaPermissionCheckInterval), (_serviceTimer) async {
|
const Duration(milliseconds: Constants.mediaPermissionCheckInterval),
|
||||||
|
(serviceTimer) async {
|
||||||
if (!super.serviceStopped) {
|
if (!super.serviceStopped) {
|
||||||
await checkEnabledAndPermission();
|
await checkEnabledAndPermission();
|
||||||
} else {
|
} else {
|
||||||
_serviceTimer.cancel();
|
serviceTimer.cancel();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_logger.d('PermissionService started');
|
_logger.d('PermissionService started');
|
||||||
|
@ -122,6 +116,29 @@ class PermissionService extends StoppableService {
|
||||||
_logger.d('PermissionService stopped');
|
_logger.d('PermissionService stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future _determineDeviceInfo() async {
|
||||||
|
if (_deviceInformationInitialized) {
|
||||||
|
_logger.d('Device information already initialized, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin();
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||||
|
if (androidInfo.version.sdkInt >= 33) {
|
||||||
|
_useStoragePermission = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_useStoragePermission) {
|
||||||
|
_logger.d('Device requires [storage] permission');
|
||||||
|
} else {
|
||||||
|
_logger.d('Device requires [photos,videos,audio] permission');
|
||||||
|
}
|
||||||
|
|
||||||
|
_deviceInformationInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
void _removeServiceCheckTimer() {
|
void _removeServiceCheckTimer() {
|
||||||
if (_serviceCheckTimer != null) {
|
if (_serviceCheckTimer != null) {
|
||||||
_serviceCheckTimer!.cancel();
|
_serviceCheckTimer!.cancel();
|
||||||
|
|
|
@ -3,7 +3,8 @@ import 'dart:async';
|
||||||
import '../enums/refresh_event.dart';
|
import '../enums/refresh_event.dart';
|
||||||
|
|
||||||
class RefreshService {
|
class RefreshService {
|
||||||
StreamController<RefreshEvent> refreshEventController = StreamController<RefreshEvent>.broadcast();
|
StreamController<RefreshEvent> refreshEventController =
|
||||||
|
StreamController<RefreshEvent>.broadcast();
|
||||||
|
|
||||||
void addEvent(RefreshEvent event) {
|
void addEvent(RefreshEvent event) {
|
||||||
if (refreshEventController.hasListener) {
|
if (refreshEventController.hasListener) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ class SessionService extends StoppableService {
|
||||||
Future<bool> login(String url, String apiKey) async {
|
Future<bool> login(String url, String apiKey) async {
|
||||||
setApiConfig(url, apiKey);
|
setApiConfig(url, apiKey);
|
||||||
|
|
||||||
var session = new Session(url: url, apiKey: apiKey);
|
var session = Session(url: url, apiKey: apiKey);
|
||||||
sessionController.add(session);
|
sessionController.add(session);
|
||||||
await _storageService.storeSession(session);
|
await _storageService.storeSession(session);
|
||||||
_logger.d('Session created');
|
_logger.d('Session created');
|
||||||
|
|
|
@ -5,45 +5,36 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../models/session.dart';
|
import '../models/session.dart';
|
||||||
|
|
||||||
class StorageService {
|
class StorageService {
|
||||||
static const _SESSION_KEY = 'session';
|
static const _sessionKey = 'session';
|
||||||
static const _LAST_URL_KEY = 'last_url';
|
static const _lastUrlKey = 'last_url';
|
||||||
static const _STORAGE_PERMISSION_DIALOG_IGNORED = 'storage_permission_ignored';
|
|
||||||
|
|
||||||
Future<bool> storeLastUrl(String url) {
|
Future<bool> storeLastUrl(String url) {
|
||||||
return _store(_LAST_URL_KEY, url);
|
return _store(_lastUrlKey, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> retrieveLastUrl() async {
|
Future<String?> retrieveLastUrl() async {
|
||||||
return await _retrieve(_LAST_URL_KEY);
|
return await _retrieve(_lastUrlKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasLastUrl() async {
|
Future<bool> hasLastUrl() async {
|
||||||
return await _exists(_LAST_URL_KEY);
|
return await _exists(_lastUrlKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> storeSession(Session session) {
|
Future<bool> storeSession(Session session) {
|
||||||
return _store(_SESSION_KEY, json.encode(session));
|
return _store(_sessionKey, json.encode(session));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Session> retrieveSession() async {
|
Future<Session> retrieveSession() async {
|
||||||
var retrieve = await _retrieve(_SESSION_KEY);
|
var retrieve = await _retrieve(_sessionKey);
|
||||||
return Session.fromJson(json.decode(retrieve!));
|
return Session.fromJson(json.decode(retrieve!));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> hasSession() {
|
Future<bool> hasSession() {
|
||||||
return _exists(_SESSION_KEY);
|
return _exists(_sessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> removeSession() {
|
Future<bool> removeSession() {
|
||||||
return _remove(_SESSION_KEY);
|
return _remove(_sessionKey);
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> storeStoragePermissionDialogIgnored() {
|
|
||||||
return _store(_STORAGE_PERMISSION_DIALOG_IGNORED, true.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasStoragePermissionDialogIgnored() {
|
|
||||||
return _exists(_STORAGE_PERMISSION_DIALOG_IGNORED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _exists(String key) async {
|
Future<bool> _exists(String key) async {
|
||||||
|
|
|
@ -12,9 +12,10 @@ class UserService {
|
||||||
final FileService _fileService = locator<FileService>();
|
final FileService _fileService = locator<FileService>();
|
||||||
final UserRepository _userRepository = locator<UserRepository>();
|
final UserRepository _userRepository = locator<UserRepository>();
|
||||||
|
|
||||||
Future<CreateApiKeyResponse> createApiKey(
|
Future<CreateApiKeyResponse> createApiKey(String url, String username,
|
||||||
String url, String username, String password, String accessLevel, String comment) async {
|
String password, String accessLevel, String comment) async {
|
||||||
return await _userRepository.postApiKey(url, username, password, accessLevel, comment);
|
return await _userRepository.postApiKey(
|
||||||
|
url, username, password, accessLevel, comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiKeysResponse> getApiKeys() async {
|
Future<ApiKeysResponse> getApiKeys() async {
|
||||||
|
@ -26,7 +27,7 @@ class UserService {
|
||||||
try {
|
try {
|
||||||
await _fileService.getHistory();
|
await _fileService.getHistory();
|
||||||
} on ServiceException catch (e) {
|
} 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
|
/// Format epoch timestamp
|
||||||
static String formatEpoch(num millis) {
|
static String formatEpoch(num millis) {
|
||||||
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm();
|
DateFormat dateFormat = DateFormat().add_yMEd().add_Hm();
|
||||||
return dateFormat.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
|
return dateFormat
|
||||||
|
.format(DateTime.fromMillisecondsSinceEpoch(millis as int));
|
||||||
}
|
}
|
||||||
|
|
||||||
static String formatBytes(int bytes, int decimals) {
|
static String formatBytes(int bytes, int decimals) {
|
||||||
if (bytes <= 0) return "0 B";
|
if (bytes <= 0) return "0 B";
|
||||||
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
var i = (log(bytes) / log(1024)).floor();
|
var i = (log(bytes) / log(1024)).floor();
|
||||||
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
|
return '${(bytes / pow(1024, i)).toStringAsFixed(decimals)} ${suffixes[i]}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,10 @@ class AboutModel extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initPackageInfo() async {
|
Future<void> _initPackageInfo() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
final PackageInfo info = await PackageInfo.fromPlatform();
|
final PackageInfo info = await PackageInfo.fromPlatform();
|
||||||
packageInfo = info;
|
packageInfo = info;
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openLink(String link) {
|
void openLink(String link) {
|
||||||
|
|
|
@ -5,18 +5,21 @@ import '../../core/util/logger.dart';
|
||||||
import '../enums/viewstate.dart';
|
import '../enums/viewstate.dart';
|
||||||
|
|
||||||
class BaseModel extends ChangeNotifier {
|
class BaseModel extends ChangeNotifier {
|
||||||
static const String STATE_VIEW = 'viewState';
|
static const String stateViewKey = 'viewState';
|
||||||
static const String STATE_MESSAGE = 'viewMessage';
|
static const String stateMessageKey = 'viewMessage';
|
||||||
|
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
|
|
||||||
bool _isDisposed = false;
|
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) {
|
bool getStateValueAsBoolean(String key) {
|
||||||
if (_stateMap.containsKey(key) && _stateMap[key] is bool) {
|
if (_stateMap.containsKey(key) && _stateMap[key] is bool) {
|
||||||
|
@ -42,11 +45,14 @@ class BaseModel extends ChangeNotifier {
|
||||||
return null;
|
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) {
|
void _setStateValue(String key, Object? stateValue) {
|
||||||
if (_stateMap.containsKey(key)) {
|
if (_stateMap.containsKey(key)) {
|
||||||
|
@ -57,7 +63,8 @@ class BaseModel extends ChangeNotifier {
|
||||||
|
|
||||||
if (!_isDisposed) {
|
if (!_isDisposed) {
|
||||||
notifyListeners();
|
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) {
|
void setStateView(ViewState stateView) {
|
||||||
_setStateValue(STATE_VIEW, stateView);
|
_setStateValue(stateViewKey, stateView);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setStateMessage(String? stateMessage) {
|
void setStateMessage(String? stateMessage) {
|
||||||
_setStateValue(STATE_MESSAGE, stateMessage);
|
_setStateValue(stateMessageKey, stateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_logger.d("Calling dispose");
|
||||||
super.dispose();
|
super.dispose();
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,9 @@ class HistoryModel extends BaseModel {
|
||||||
String? errorMessage;
|
String? errorMessage;
|
||||||
|
|
||||||
void init() {
|
void init() {
|
||||||
_refreshTriggerSubscription = _refreshService.refreshEventController.stream.listen((event) {
|
_refreshTriggerSubscription =
|
||||||
if (event == RefreshEvent.RefreshHistory) {
|
_refreshService.refreshEventController.stream.listen((event) {
|
||||||
|
if (event == RefreshEvent.refreshHistory) {
|
||||||
_logger.d('History needs a refresh');
|
_logger.d('History needs a refresh');
|
||||||
getHistory();
|
getHistory();
|
||||||
}
|
}
|
||||||
|
@ -43,18 +44,19 @@ class HistoryModel extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future getHistory() async {
|
Future getHistory() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pastes.clear();
|
pastes.clear();
|
||||||
History _history = await _fileService.getHistory();
|
History history = await _fileService.getHistory();
|
||||||
if (_history.items.isNotEmpty) {
|
if (history.items.isNotEmpty) {
|
||||||
_history.items.forEach((key, value) {
|
history.items.forEach((key, value) {
|
||||||
var millisecondsSinceEpoch = int.parse(value.date) * 1000;
|
var millisecondsSinceEpoch = int.parse(value.date) * 1000;
|
||||||
pastes.add(
|
pastes.add(
|
||||||
UploadedPaste(
|
UploadedPaste(
|
||||||
id: key,
|
id: key,
|
||||||
date: DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
|
date:
|
||||||
|
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch),
|
||||||
filename: value.filename,
|
filename: value.filename,
|
||||||
filesize: int.parse(value.filesize),
|
filesize: int.parse(value.filesize),
|
||||||
hash: value.hash,
|
hash: value.hash,
|
||||||
|
@ -66,8 +68,8 @@ class HistoryModel extends BaseModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_history.multipasteItems.isNotEmpty) {
|
if (history.multipasteItems.isNotEmpty) {
|
||||||
_history.multipasteItems.forEach((key, multiPaste) {
|
history.multipasteItems.forEach((key, multiPaste) {
|
||||||
var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000;
|
var millisecondsSinceEpoch = int.parse(multiPaste.date) * 1000;
|
||||||
pastes.add(UploadedPaste(
|
pastes.add(UploadedPaste(
|
||||||
id: key,
|
id: key,
|
||||||
|
@ -90,32 +92,35 @@ class HistoryModel extends BaseModel {
|
||||||
e.responseBody is RestError &&
|
e.responseBody is RestError &&
|
||||||
e.responseBody.message != null) {
|
e.responseBody.message != null) {
|
||||||
if (e.statusCode == HttpStatus.badRequest) {
|
if (e.statusCode == HttpStatus.badRequest) {
|
||||||
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
|
errorMessage = translate('api.bad_request',
|
||||||
|
args: {'reason': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message});
|
errorMessage = translate('api.general_rest_error_payload',
|
||||||
|
args: {'message': e.responseBody.message});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||||
errorMessage = translate('api.socket_error');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', error: e);
|
||||||
throw e;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future deletePaste(String id) async {
|
Future deletePaste(String id) async {
|
||||||
DialogResponse res = await _dialogService.showConfirmationDialog(
|
DialogResponse res = await _dialogService.showConfirmationDialog(
|
||||||
title: translate('history.delete_dialog.title'),
|
title: translate('history.delete_dialog.title'),
|
||||||
description: translate('history.delete_dialog.description', args: {'id': id}),
|
description:
|
||||||
|
translate('history.delete_dialog.description', args: {'id': id}),
|
||||||
buttonTitleAccept: translate('history.delete_dialog.accept'),
|
buttonTitleAccept: translate('history.delete_dialog.accept'),
|
||||||
buttonTitleDeny: translate('history.delete_dialog.deny'));
|
buttonTitleDeny: translate('history.delete_dialog.deny'));
|
||||||
|
|
||||||
|
@ -123,7 +128,7 @@ class HistoryModel extends BaseModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _fileService.deletePaste(id);
|
await _fileService.deletePaste(id);
|
||||||
|
@ -139,23 +144,24 @@ class HistoryModel extends BaseModel {
|
||||||
e.statusCode != HttpStatus.forbidden &&
|
e.statusCode != HttpStatus.forbidden &&
|
||||||
e.responseBody is RestError &&
|
e.responseBody is RestError &&
|
||||||
e.responseBody.message != null) {
|
e.responseBody.message != null) {
|
||||||
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message});
|
errorMessage = translate('api.general_rest_error_payload',
|
||||||
|
args: {'message': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||||
errorMessage = translate('api.socket_error');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', error: e);
|
||||||
throw e;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openLink(String link) {
|
void openLink(String link) {
|
||||||
|
|
|
@ -20,12 +20,12 @@ import '../util/logger.dart';
|
||||||
import 'base_model.dart';
|
import 'base_model.dart';
|
||||||
|
|
||||||
class LoginModel extends BaseModel {
|
class LoginModel extends BaseModel {
|
||||||
TextEditingController _uriController = new TextEditingController();
|
TextEditingController _uriController = TextEditingController();
|
||||||
|
|
||||||
final TextEditingController _userNameController = new TextEditingController();
|
final TextEditingController _userNameController = TextEditingController();
|
||||||
final TextEditingController _passwordController = new TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
|
||||||
final TextEditingController _apiKeyController = new TextEditingController();
|
final TextEditingController _apiKeyController = TextEditingController();
|
||||||
|
|
||||||
TextEditingController get uriController => _uriController;
|
TextEditingController get uriController => _uriController;
|
||||||
|
|
||||||
|
@ -44,23 +44,23 @@ class LoginModel extends BaseModel {
|
||||||
String? errorMessage;
|
String? errorMessage;
|
||||||
|
|
||||||
void toggleLoginMethod() {
|
void toggleLoginMethod() {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
useCredentialsLogin = !useCredentialsLogin;
|
useCredentialsLogin = !useCredentialsLogin;
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
bool hasLastUrl = await _storageService.hasLastUrl();
|
bool hasLastUrl = await _storageService.hasLastUrl();
|
||||||
|
|
||||||
if (hasLastUrl) {
|
if (hasLastUrl) {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
|
var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
|
||||||
|
|
||||||
if (s.isNotEmpty) {
|
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 password = passwordController.text;
|
||||||
var apiKey = apiKeyController.text;
|
var apiKey = apiKeyController.text;
|
||||||
|
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
url = trim(url);
|
url = trim(url);
|
||||||
username = trim(username);
|
username = trim(username);
|
||||||
|
|
||||||
if (url.isEmpty) {
|
if (url.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_url');
|
errorMessage = translate('login.errors.empty_url');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!url.contains("https://") && !url.contains("http://")) {
|
if (!url.contains("https://") && !url.contains("http://")) {
|
||||||
errorMessage = translate('login.errors.no_protocol');
|
errorMessage = translate('login.errors.no_protocol');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool validUri = Uri.parse(url).isAbsolute;
|
bool validUri = Uri.parse(url).isAbsolute;
|
||||||
if (!validUri || !isURL(url)) {
|
if (!validUri || !isURL(url)) {
|
||||||
errorMessage = translate('login.errors.invalid_url');
|
errorMessage = translate('login.errors.invalid_url');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useCredentialsLogin) {
|
if (useCredentialsLogin) {
|
||||||
if (username.isEmpty) {
|
if (username.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_username');
|
errorMessage = translate('login.errors.empty_username');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.isEmpty) {
|
if (password.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_password');
|
errorMessage = translate('login.errors.empty_password');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (apiKey.isEmpty) {
|
if (apiKey.isEmpty) {
|
||||||
errorMessage = translate('login.errors.empty_apikey');
|
errorMessage = translate('login.errors.empty_apikey');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,14 +117,19 @@ class LoginModel extends BaseModel {
|
||||||
try {
|
try {
|
||||||
if (useCredentialsLogin) {
|
if (useCredentialsLogin) {
|
||||||
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
|
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'];
|
var newKey = apiKeyResponse.data['new_key'];
|
||||||
if (newKey != null) {
|
if (newKey != null) {
|
||||||
success = await _sessionService.login(url, newKey);
|
success = await _sessionService.login(url, newKey);
|
||||||
} else {
|
} else {
|
||||||
throw new ServiceException(
|
throw ServiceException(
|
||||||
code: ErrorCode.INVALID_API_KEY, message: translate('login.errors.invalid_api_key'));
|
code: ErrorCode.invalidApiKey,
|
||||||
|
message: translate('login.errors.invalid_api_key'));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_sessionService.setApiConfig(url, apiKey);
|
_sessionService.setApiConfig(url, apiKey);
|
||||||
|
@ -136,35 +141,37 @@ class LoginModel extends BaseModel {
|
||||||
if (e is RestServiceException) {
|
if (e is RestServiceException) {
|
||||||
if (e.statusCode == HttpStatus.unauthorized) {
|
if (e.statusCode == HttpStatus.unauthorized) {
|
||||||
errorMessage = translate('login.errors.wrong_credentials');
|
errorMessage = translate('login.errors.wrong_credentials');
|
||||||
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
|
} else if (e.statusCode != HttpStatus.unauthorized &&
|
||||||
|
e.statusCode == HttpStatus.forbidden) {
|
||||||
errorMessage = translate('login.errors.forbidden');
|
errorMessage = translate('login.errors.forbidden');
|
||||||
} else if (e.statusCode == HttpStatus.notFound) {
|
} else if (e.statusCode == HttpStatus.notFound) {
|
||||||
errorMessage = translate('api.incompatible_error_not_found');
|
errorMessage = translate('api.incompatible_error_not_found');
|
||||||
}
|
}
|
||||||
if (e.statusCode == HttpStatus.badRequest) {
|
if (e.statusCode == HttpStatus.badRequest) {
|
||||||
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
|
errorMessage = translate('api.bad_request',
|
||||||
|
args: {'reason': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.INVALID_API_KEY) {
|
} else if (e is ServiceException && e.code == ErrorCode.invalidApiKey) {
|
||||||
errorMessage = translate('login.errors.invalid_api_key');
|
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');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', error: e);
|
||||||
throw e;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage!.isNotEmpty) {
|
if (errorMessage!.isNotEmpty) {
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,8 @@ class ProfileModel extends BaseModel {
|
||||||
|
|
||||||
Future logout() async {
|
Future logout() async {
|
||||||
var dialogResult = await _dialogService.showConfirmationDialog(
|
var dialogResult = await _dialogService.showConfirmationDialog(
|
||||||
title: translate('logout.title'), description: translate('logout.confirm'));
|
title: translate('logout.title'),
|
||||||
|
description: translate('logout.confirm'));
|
||||||
|
|
||||||
if (dialogResult.confirmed!) {
|
if (dialogResult.confirmed!) {
|
||||||
await _sessionService.logout();
|
await _sessionService.logout();
|
||||||
|
@ -41,7 +42,8 @@ class ProfileModel extends BaseModel {
|
||||||
Future revealApiKey(String? apiKey) async {
|
Future revealApiKey(String? apiKey) async {
|
||||||
await _dialogService.showDialog(
|
await _dialogService.showDialog(
|
||||||
title: translate('profile.revealed_api_key.title'),
|
title: translate('profile.revealed_api_key.title'),
|
||||||
description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey}));
|
description: translate('profile.revealed_api_key.description',
|
||||||
|
args: {'apiKey': apiKey}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future showConfig(String url) async {
|
Future showConfig(String url) async {
|
||||||
|
@ -54,27 +56,29 @@ class ProfileModel extends BaseModel {
|
||||||
if (e is RestServiceException) {
|
if (e is RestServiceException) {
|
||||||
if (e.statusCode == HttpStatus.unauthorized) {
|
if (e.statusCode == HttpStatus.unauthorized) {
|
||||||
errorMessage = translate('login.errors.wrong_credentials');
|
errorMessage = translate('login.errors.wrong_credentials');
|
||||||
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
|
} else if (e.statusCode != HttpStatus.unauthorized &&
|
||||||
|
e.statusCode == HttpStatus.forbidden) {
|
||||||
errorMessage = translate('login.errors.forbidden');
|
errorMessage = translate('login.errors.forbidden');
|
||||||
} else if (e.statusCode == HttpStatus.notFound) {
|
} else if (e.statusCode == HttpStatus.notFound) {
|
||||||
errorMessage = translate('api.incompatible_error_not_found');
|
errorMessage = translate('api.incompatible_error_not_found');
|
||||||
}
|
}
|
||||||
if (e.statusCode == HttpStatus.badRequest) {
|
if (e.statusCode == HttpStatus.badRequest) {
|
||||||
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
|
errorMessage = translate('api.bad_request',
|
||||||
|
args: {'reason': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||||
errorMessage = translate('api.socket_error');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setStateBoolValue(_configurationButtonLoading, false);
|
setStateBoolValue(_configurationButtonLoading, false);
|
||||||
_sessionService.logout();
|
_sessionService.logout();
|
||||||
setStateBoolValue(_configurationButtonLoading, false);
|
setStateBoolValue(_configurationButtonLoading, false);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', error: e);
|
||||||
throw e;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,15 +88,18 @@ class ProfileModel extends BaseModel {
|
||||||
await _dialogService.showDialog(
|
await _dialogService.showDialog(
|
||||||
title: translate('profile.shown_config.title'),
|
title: translate('profile.shown_config.title'),
|
||||||
description: translate('profile.shown_config.description', args: {
|
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,
|
'maxFilesPerRequest': config.maxFilesPerRequest,
|
||||||
'maxInputVars': config.maxInputVars,
|
'maxInputVars': config.maxInputVars,
|
||||||
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
|
'requestMaxSize':
|
||||||
|
FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
await _dialogService.showDialog(
|
await _dialogService.showDialog(
|
||||||
title: translate('profile.shown_config.error.title'),
|
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 'package:flutter_translate/flutter_translate.dart';
|
||||||
|
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
|
@ -9,19 +10,21 @@ import 'base_model.dart';
|
||||||
|
|
||||||
class StartUpViewModel extends BaseModel {
|
class StartUpViewModel extends BaseModel {
|
||||||
final SessionService _sessionService = locator<SessionService>();
|
final SessionService _sessionService = locator<SessionService>();
|
||||||
|
final PermissionService _permissionService = locator<PermissionService>();
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
Future handleStartUpLogic() async {
|
Future handleStartUpLogic() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
setStateMessage(translate('startup.init'));
|
setStateMessage(translate('startup.init'));
|
||||||
await Future.delayed(Duration(milliseconds: 150));
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
setStateMessage(translate('startup.start_services'));
|
setStateMessage(translate('startup.start_services'));
|
||||||
await _sessionService.start();
|
await _sessionService.start();
|
||||||
await Future.delayed(Duration(milliseconds: 150));
|
await _permissionService.start();
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
||||||
|
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ import 'dart:io';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
|
||||||
|
import 'package:flutter_sharing_intent/model/sharing_file.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
|
||||||
|
|
||||||
import '../../locator.dart';
|
import '../../locator.dart';
|
||||||
import '../enums/error_code.dart';
|
import '../enums/error_code.dart';
|
||||||
|
@ -31,7 +32,7 @@ class UploadModel extends BaseModel {
|
||||||
final LinkService _linkService = locator<LinkService>();
|
final LinkService _linkService = locator<LinkService>();
|
||||||
final RefreshService _refreshService = locator<RefreshService>();
|
final RefreshService _refreshService = locator<RefreshService>();
|
||||||
|
|
||||||
TextEditingController _pasteTextController = TextEditingController();
|
final TextEditingController _pasteTextController = TextEditingController();
|
||||||
bool pasteTextTouched = false;
|
bool pasteTextTouched = false;
|
||||||
|
|
||||||
late StreamSubscription _intentDataStreamSubscription;
|
late StreamSubscription _intentDataStreamSubscription;
|
||||||
|
@ -45,6 +46,41 @@ class UploadModel extends BaseModel {
|
||||||
|
|
||||||
TextEditingController get pasteTextController => _pasteTextController;
|
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() {
|
void init() {
|
||||||
_pasteTextController.addListener(() {
|
_pasteTextController.addListener(() {
|
||||||
pasteTextTouched = pasteTextController.text.isNotEmpty;
|
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
|
// For sharing images coming from outside the app while the app is in the memory
|
||||||
_intentDataStreamSubscription = ReceiveSharingIntent.getMediaStream().listen((List<SharedMediaFile> value) {
|
_intentDataStreamSubscription = FlutterSharingIntent.instance
|
||||||
if (value.length > 0) {
|
.getMediaStream()
|
||||||
setStateView(ViewState.Busy);
|
.listen((List<SharedFile> value) {
|
||||||
paths = value.map((sharedFile) {
|
_logger.d("Retrieved ${value.length} files from intent");
|
||||||
return PlatformFile.fromMap({
|
_parseIntentFiles(value);
|
||||||
'path': sharedFile.path,
|
|
||||||
'name': basename(sharedFile.path),
|
|
||||||
'size': File(sharedFile.path).lengthSync(),
|
|
||||||
'bytes': null
|
|
||||||
});
|
|
||||||
}).toList();
|
|
||||||
setStateView(ViewState.Idle);
|
|
||||||
}
|
|
||||||
}, onError: (err) {
|
}, onError: (err) {
|
||||||
_errorIntentHandle(err);
|
_errorIntentHandle(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For sharing images coming from outside the app while the app is closed
|
// For sharing images coming from outside the app while the app is closed
|
||||||
ReceiveSharingIntent.getInitialMedia().then((List<SharedMediaFile> value) {
|
FlutterSharingIntent.instance
|
||||||
if (value.length > 0) {
|
.getInitialSharing()
|
||||||
setStateView(ViewState.Busy);
|
.then((List<SharedFile> value) {
|
||||||
paths = value.map((sharedFile) {
|
_logger.d("Retrieved ${value.length} files from inactive intent");
|
||||||
return PlatformFile.fromMap({
|
_parseIntentFiles(value);
|
||||||
'path': sharedFile.path,
|
|
||||||
'name': basename(sharedFile.path),
|
|
||||||
'size': File(sharedFile.path).lengthSync(),
|
|
||||||
'bytes': null
|
|
||||||
});
|
|
||||||
}).toList();
|
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _errorIntentHandle(err) {
|
void _errorIntentHandle(err) {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
errorMessage = translate('upload.retrieval_intent');
|
errorMessage = translate('upload.retrieval_intent');
|
||||||
_logger.e('Error while retrieving shared data: $err');
|
_logger.e('Error while retrieving shared data: $err');
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? generatePasteLinks(Map<String, bool>? uploads, String url) {
|
String? generatePasteLinks(Map<String, bool>? uploads, String url) {
|
||||||
if (uploads != null && uploads.length > 0) {
|
if (uploads != null && uploads.isNotEmpty) {
|
||||||
var links = '';
|
var links = '';
|
||||||
|
|
||||||
uploads.forEach((id, isMulti) {
|
uploads.forEach((id, isMulti) {
|
||||||
|
@ -134,13 +130,13 @@ class UploadModel extends BaseModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleCreateMulti() {
|
void toggleCreateMulti() {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
createMulti = !createMulti;
|
createMulti = !createMulti;
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void openFileExplorer() async {
|
void openFileExplorer() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
setStateMessage(translate('upload.file_explorer_open'));
|
setStateMessage(translate('upload.file_explorer_open'));
|
||||||
loadingPath = true;
|
loadingPath = true;
|
||||||
|
|
||||||
|
@ -150,62 +146,68 @@ class UploadModel extends BaseModel {
|
||||||
allowMultiple: true,
|
allowMultiple: true,
|
||||||
withData: false,
|
withData: false,
|
||||||
withReadStream: true,
|
withReadStream: true,
|
||||||
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '').split(',') : null,
|
allowedExtensions: (_extension?.isNotEmpty ?? false)
|
||||||
|
? _extension?.replaceAll(' ', '').split(',')
|
||||||
|
: null,
|
||||||
))
|
))
|
||||||
?.files;
|
?.files;
|
||||||
} on PlatformException catch (e) {
|
} on PlatformException catch (e) {
|
||||||
_logger.e('Unsupported operation', e);
|
_logger.e('Unsupported operation', error: e);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
_logger.e('An unknown error occurred', ex);
|
_logger.e('An unknown error occurred', error: ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingPath = false;
|
loadingPath = false;
|
||||||
fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
|
fileName = paths != null ? paths!.map((e) => e.name).toString() : '...';
|
||||||
|
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearCachedFiles() async {
|
void clearCachedFiles() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
await FilePicker.platform.clearTemporaryFiles();
|
await FilePicker.platform.clearTemporaryFiles();
|
||||||
paths = null;
|
paths = null;
|
||||||
fileName = null;
|
fileName = null;
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, bool>?> upload() async {
|
Future<Map<String, bool>?> upload() async {
|
||||||
setStateView(ViewState.Busy);
|
setStateView(ViewState.busy);
|
||||||
setStateMessage(translate('upload.uploading_now'));
|
setStateMessage(translate('upload.uploading_now'));
|
||||||
|
|
||||||
Map<String, bool> uploadedPasteIds = new Map();
|
Map<String, bool> uploadedPasteIds = {};
|
||||||
try {
|
try {
|
||||||
List<File>? files;
|
List<File>? files;
|
||||||
Map<String, String>? additionalFiles;
|
Map<String, String>? additionalFiles;
|
||||||
|
|
||||||
if (pasteTextController.text.isNotEmpty) {
|
if (pasteTextController.text.isNotEmpty) {
|
||||||
additionalFiles = Map.from(
|
additionalFiles = Map.from({
|
||||||
{'paste-${(new DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt': pasteTextController.text});
|
'paste-${(DateTime.now().millisecondsSinceEpoch / 1000).round()}.txt':
|
||||||
}
|
pasteTextController.text
|
||||||
|
|
||||||
if (paths != null && paths!.length > 0) {
|
|
||||||
files = paths!.map((e) => new File(e.path!)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
UploadedResponse response = await _fileService.uploadPaste(files, additionalFiles);
|
|
||||||
response.data.ids.forEach((element) {
|
|
||||||
uploadedPasteIds.putIfAbsent(element, () => false);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paths != null && paths!.isNotEmpty) {
|
||||||
|
files = paths!.map((e) => File(e.path!)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadedResponse response =
|
||||||
|
await _fileService.uploadPaste(files, additionalFiles);
|
||||||
|
for (var element in response.data.ids) {
|
||||||
|
uploadedPasteIds.putIfAbsent(element, () => false);
|
||||||
|
}
|
||||||
|
|
||||||
if (createMulti && response.data.ids.length > 1) {
|
if (createMulti && response.data.ids.length > 1) {
|
||||||
UploadedMultiResponse multiResponse = await _fileService.uploadMultiPaste(response.data.ids);
|
UploadedMultiResponse multiResponse =
|
||||||
|
await _fileService.uploadMultiPaste(response.data.ids);
|
||||||
uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true);
|
uploadedPasteIds.putIfAbsent(multiResponse.data.urlId, () => true);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCachedFiles();
|
clearCachedFiles();
|
||||||
_pasteTextController.clear();
|
_pasteTextController.clear();
|
||||||
_refreshService.addEvent(RefreshEvent.RefreshHistory);
|
_refreshService.addEvent(RefreshEvent.refreshHistory);
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
return uploadedPasteIds;
|
return uploadedPasteIds;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -219,28 +221,30 @@ class UploadModel extends BaseModel {
|
||||||
e.responseBody is RestError &&
|
e.responseBody is RestError &&
|
||||||
e.responseBody.message != null) {
|
e.responseBody.message != null) {
|
||||||
if (e.statusCode == HttpStatus.badRequest) {
|
if (e.statusCode == HttpStatus.badRequest) {
|
||||||
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
|
errorMessage = translate('api.bad_request',
|
||||||
|
args: {'reason': e.responseBody.message});
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error_payload', args: {'message': e.responseBody.message});
|
errorMessage = translate('api.general_rest_error_payload',
|
||||||
|
args: {'message': e.responseBody.message});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('api.general_rest_error');
|
errorMessage = translate('api.general_rest_error');
|
||||||
}
|
}
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
|
||||||
errorMessage = translate('api.socket_error');
|
errorMessage = translate('api.socket_error');
|
||||||
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
|
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
|
||||||
errorMessage = translate('api.socket_timeout');
|
errorMessage = translate('api.socket_timeout');
|
||||||
} else {
|
} else {
|
||||||
errorMessage = translate('app.unknown_error');
|
errorMessage = translate('app.unknown_error');
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
_logger.e('An unknown error occurred', e);
|
_logger.e('An unknown error occurred', error: e);
|
||||||
throw e;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStateMessage(null);
|
setStateMessage(null);
|
||||||
setStateView(ViewState.Idle);
|
setStateView(ViewState.idle);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,6 +256,8 @@ class UploadModel extends BaseModel {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pasteTextController.dispose();
|
_pasteTextController.dispose();
|
||||||
_intentDataStreamSubscription.cancel();
|
_intentDataStreamSubscription.cancel();
|
||||||
|
FlutterSharingIntent.instance.reset();
|
||||||
|
paths = null;
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ void main() async {
|
||||||
setupLogger(Level.info);
|
setupLogger(Level.info);
|
||||||
setupLocator();
|
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();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
runApp(LocalizedApp(delegate, MyApp()));
|
runApp(LocalizedApp(delegate, MyApp()));
|
||||||
|
|
|
@ -14,20 +14,21 @@ class AppRouter {
|
||||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||||
switch (settings.name) {
|
switch (settings.name) {
|
||||||
case StartUpView.routeName:
|
case StartUpView.routeName:
|
||||||
return MaterialPageRoute(builder: (_) => StartUpView());
|
return MaterialPageRoute(builder: (_) => const StartUpView());
|
||||||
case AboutView.routeName:
|
case AboutView.routeName:
|
||||||
return MaterialPageRoute(builder: (_) => AboutView());
|
return MaterialPageRoute(builder: (_) => const AboutView());
|
||||||
case HomeView.routeName:
|
case HomeView.routeName:
|
||||||
return MaterialPageRoute(builder: (_) => TabBarContainerView());
|
return MaterialPageRoute(builder: (_) => const TabBarContainerView());
|
||||||
case LoginView.routeName:
|
case LoginView.routeName:
|
||||||
return MaterialPageRoute(builder: (_) => LoginView());
|
return MaterialPageRoute(builder: (_) => LoginView());
|
||||||
case ProfileView.routeName:
|
case ProfileView.routeName:
|
||||||
return MaterialPageRoute(builder: (_) => ProfileView());
|
return MaterialPageRoute(builder: (_) => const ProfileView());
|
||||||
default:
|
default:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) => Scaffold(
|
builder: (_) => Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Text(translate('dev.no_route', args: {'route': settings.name})),
|
child: Text(translate('dev.no_route',
|
||||||
|
args: {'route': settings.name})),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class UIHelper {
|
class UIHelper {
|
||||||
static const double _VerticalSpaceSmall = 10.0;
|
static const double _verticalSpaceSmall = 10.0;
|
||||||
static const double _VerticalSpaceMedium = 20.0;
|
static const double _verticalSpaceMedium = 20.0;
|
||||||
static const double _VerticalSpaceLarge = 60.0;
|
static const double _verticalSpaceLarge = 60.0;
|
||||||
|
|
||||||
static const double _HorizontalSpaceSmall = 10.0;
|
static const double _horizontalSpaceSmall = 10.0;
|
||||||
static const double _HorizontalSpaceMedium = 20.0;
|
static const double _horizontalSpaceMedium = 20.0;
|
||||||
static const double HorizontalSpaceLarge = 60.0;
|
static const double _horizontalSpaceLarge = 60.0;
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [_VerticalSpaceSmall]
|
/// Returns a vertical space with height set to [_verticalSpaceSmall]
|
||||||
static Widget verticalSpaceSmall() {
|
static Widget verticalSpaceSmall() {
|
||||||
return verticalSpace(_VerticalSpaceSmall);
|
return verticalSpace(_verticalSpaceSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [_VerticalSpaceMedium]
|
/// Returns a vertical space with height set to [_verticalSpaceMedium]
|
||||||
static Widget verticalSpaceMedium() {
|
static Widget verticalSpaceMedium() {
|
||||||
return verticalSpace(_VerticalSpaceMedium);
|
return verticalSpace(_verticalSpaceMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [_VerticalSpaceLarge]
|
/// Returns a vertical space with height set to [_verticalSpaceLarge]
|
||||||
static Widget verticalSpaceLarge() {
|
static Widget verticalSpaceLarge() {
|
||||||
return verticalSpace(_VerticalSpaceLarge);
|
return verticalSpace(_verticalSpaceLarge);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space equal to the [height] supplied
|
/// Returns a vertical space equal to the [height] supplied
|
||||||
|
@ -29,19 +29,19 @@ class UIHelper {
|
||||||
return Container(height: height);
|
return Container(height: height);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [_HorizontalSpaceSmall]
|
/// Returns a vertical space with height set to [_horizontalSpaceSmall]
|
||||||
static Widget horizontalSpaceSmall() {
|
static Widget horizontalSpaceSmall() {
|
||||||
return horizontalSpace(_HorizontalSpaceSmall);
|
return horizontalSpace(_horizontalSpaceSmall);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [_HorizontalSpaceMedium]
|
/// Returns a vertical space with height set to [_horizontalSpaceMedium]
|
||||||
static Widget horizontalSpaceMedium() {
|
static Widget horizontalSpaceMedium() {
|
||||||
return horizontalSpace(_HorizontalSpaceMedium);
|
return horizontalSpace(_horizontalSpaceMedium);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space with height set to [HorizontalSpaceLarge]
|
/// Returns a vertical space with height set to [_horizontalSpaceLarge]
|
||||||
static Widget horizontalSpaceLarge() {
|
static Widget horizontalSpaceLarge() {
|
||||||
return horizontalSpace(HorizontalSpaceLarge);
|
return horizontalSpace(_horizontalSpaceLarge);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a vertical space equal to the [width] supplied
|
/// Returns a vertical space equal to the [width] supplied
|
||||||
|
|
|
@ -12,6 +12,8 @@ import 'base_view.dart';
|
||||||
class AboutView extends StatelessWidget {
|
class AboutView extends StatelessWidget {
|
||||||
static const routeName = '/about';
|
static const routeName = '/about';
|
||||||
|
|
||||||
|
const AboutView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final logo = Hero(
|
final logo = Hero(
|
||||||
|
@ -30,13 +32,14 @@ class AboutView extends StatelessWidget {
|
||||||
title: Text(translate('titles.about')),
|
title: Text(translate('titles.about')),
|
||||||
enableAbout: false,
|
enableAbout: false,
|
||||||
),
|
),
|
||||||
body: model.state == ViewState.Busy
|
body: model.state == ViewState.busy
|
||||||
? Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Container(
|
: Container(
|
||||||
padding: EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.only(left: 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>[
|
children: <Widget>[
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(child: logo),
|
Center(child: logo),
|
||||||
|
@ -75,7 +78,7 @@ class AboutView extends StatelessWidget {
|
||||||
Center(
|
Center(
|
||||||
child: Linkify(
|
child: Linkify(
|
||||||
text: translate('about.website'),
|
text: translate('about.website'),
|
||||||
options: LinkifyOptions(humanize: false),
|
options: const LinkifyOptions(humanize: false),
|
||||||
onOpen: (link) => model.openLink(link.url),
|
onOpen: (link) => model.openLink(link.url),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,7 @@ class BaseView<T extends BaseModel> extends StatefulWidget {
|
||||||
final Widget Function(BuildContext context, T model, Widget? child)? builder;
|
final Widget Function(BuildContext context, T model, Widget? child)? builder;
|
||||||
final Function(T)? onModelReady;
|
final Function(T)? onModelReady;
|
||||||
|
|
||||||
BaseView({this.builder, this.onModelReady});
|
const BaseView({super.key, this.builder, this.onModelReady});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_BaseViewState<T> createState() => _BaseViewState<T>();
|
_BaseViewState<T> createState() => _BaseViewState<T>();
|
||||||
|
@ -31,7 +31,9 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<T?>(create: (context) => model, child: Consumer<T>(builder: widget.builder!));
|
return ChangeNotifierProvider<T?>(
|
||||||
|
create: (context) => model,
|
||||||
|
child: Consumer<T>(builder: widget.builder!));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -18,6 +18,8 @@ import 'base_view.dart';
|
||||||
class HistoryView extends StatelessWidget {
|
class HistoryView extends StatelessWidget {
|
||||||
static const routeName = '/history';
|
static const routeName = '/history';
|
||||||
|
|
||||||
|
const HistoryView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BaseView<HistoryModel>(
|
return BaseView<HistoryModel>(
|
||||||
|
@ -25,23 +27,25 @@ class HistoryView extends StatelessWidget {
|
||||||
model.init();
|
model.init();
|
||||||
return model.getHistory();
|
return model.getHistory();
|
||||||
},
|
},
|
||||||
builder: (context, model, child) =>
|
builder: (context, model, child) => Scaffold(
|
||||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.history'))), body: _render(model, context)),
|
appBar: MyAppBar(title: Text(translate('titles.history'))),
|
||||||
|
body: _render(model, context)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _render(HistoryModel model, BuildContext context) {
|
Widget _render(HistoryModel model, BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
var url = Provider.of<Session>(context).url;
|
||||||
|
|
||||||
return model.state == ViewState.Busy
|
return model.state == ViewState.busy
|
||||||
? Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: (model.errorMessage == null
|
: (model.errorMessage == null
|
||||||
? Container(
|
? Container(
|
||||||
padding: EdgeInsets.all(0),
|
padding: const EdgeInsets.all(0),
|
||||||
child: RefreshIndicator(
|
child: RefreshIndicator(
|
||||||
onRefresh: () async => await model.getHistory(), child: _renderItems(model, url, context)))
|
onRefresh: () async => await model.getHistory(),
|
||||||
|
child: _renderItems(model, url, context)))
|
||||||
: Container(
|
: Container(
|
||||||
padding: EdgeInsets.all(25),
|
padding: const EdgeInsets.all(25),
|
||||||
child: CenteredErrorRow(
|
child: CenteredErrorRow(
|
||||||
model.errorMessage,
|
model.errorMessage,
|
||||||
retryCallback: () => model.getHistory(),
|
retryCallback: () => model.getHistory(),
|
||||||
|
@ -51,15 +55,16 @@ class HistoryView extends StatelessWidget {
|
||||||
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
|
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
|
||||||
List<Widget> cards = [];
|
List<Widget> cards = [];
|
||||||
|
|
||||||
if (model.pastes.length > 0) {
|
if (model.pastes.isNotEmpty) {
|
||||||
model.pastes.reversed.forEach((paste) {
|
for (var paste in model.pastes.reversed) {
|
||||||
List<Widget> widgets = [];
|
List<Widget> widgets = [];
|
||||||
|
|
||||||
var fullPasteUrl = PasteUtil.generateLink(url, paste.id);
|
var fullPasteUrl = PasteUtil.generateLink(url, paste.id);
|
||||||
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
|
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
|
||||||
|
|
||||||
var dateWidget = ListTile(
|
var dateWidget = ListTile(
|
||||||
title: Text(FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
|
title: Text(
|
||||||
|
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
|
||||||
subtitle: Text(translate('history.date')),
|
subtitle: Text(translate('history.date')),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -71,7 +76,8 @@ class HistoryView extends StatelessWidget {
|
||||||
var copyWidget = ListTile(
|
var copyWidget = ListTile(
|
||||||
title: Text(translate('history.copy_link.description')),
|
title: Text(translate('history.copy_link.description')),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
|
icon: const Icon(Icons.copy,
|
||||||
|
color: blueColor, textDirection: TextDirection.ltr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
FlutterClipboard.copy(fullPasteUrl).then((value) {
|
FlutterClipboard.copy(fullPasteUrl).then((value) {
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
|
@ -83,16 +89,18 @@ class HistoryView extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content: Text(translate('history.copy_link.copied')),
|
content: Text(translate('history.copy_link.copied')),
|
||||||
duration: Duration(seconds: 10),
|
duration: const Duration(seconds: 10),
|
||||||
);
|
);
|
||||||
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
var deleteWidget = ListTile(
|
var deleteWidget = ListTile(
|
||||||
title: Text(translate('history.delete')),
|
title: Text(translate('history.delete')),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(Icons.delete, color: redColor),
|
icon: const Icon(Icons.delete, color: redColor),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
model.deletePaste(paste.id);
|
model.deletePaste(paste.id);
|
||||||
}));
|
}));
|
||||||
|
@ -120,13 +128,13 @@ class HistoryView extends StatelessWidget {
|
||||||
widgets.add(fileSizeWidget);
|
widgets.add(fileSizeWidget);
|
||||||
widgets.add(mimeTypeWidget);
|
widgets.add(mimeTypeWidget);
|
||||||
} else {
|
} else {
|
||||||
paste.items!.forEach((element) {
|
for (var element in paste.items!) {
|
||||||
widgets.add(ListTile(
|
widgets.add(ListTile(
|
||||||
title: Text(element!),
|
title: Text(element!),
|
||||||
subtitle: Text(translate('history.multipaste_element')),
|
subtitle: Text(translate('history.multipaste_element')),
|
||||||
trailing: _renderOpenInBrowser(model, '$url/$element'),
|
trailing: _renderOpenInBrowser(model, '$url/$element'),
|
||||||
));
|
));
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets.add(dateWidget);
|
widgets.add(dateWidget);
|
||||||
|
@ -135,7 +143,7 @@ class HistoryView extends StatelessWidget {
|
||||||
widgets.add(deleteWidget);
|
widgets.add(deleteWidget);
|
||||||
|
|
||||||
var expandable = ExpandableTheme(
|
var expandable = ExpandableTheme(
|
||||||
data: ExpandableThemeData(
|
data: const ExpandableThemeData(
|
||||||
iconPlacement: ExpandablePanelIconPlacement.right,
|
iconPlacement: ExpandablePanelIconPlacement.right,
|
||||||
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
headerAlignment: ExpandablePanelHeaderAlignment.center,
|
||||||
hasIcon: true,
|
hasIcon: true,
|
||||||
|
@ -146,7 +154,7 @@ class HistoryView extends StatelessWidget {
|
||||||
header: InkWell(
|
header: InkWell(
|
||||||
child: Text(
|
child: Text(
|
||||||
paste.id,
|
paste.id,
|
||||||
style: TextStyle(color: blueColor),
|
style: const TextStyle(color: blueColor),
|
||||||
textAlign: TextAlign.left,
|
textAlign: TextAlign.left,
|
||||||
)),
|
)),
|
||||||
expanded: Column(
|
expanded: Column(
|
||||||
|
@ -162,15 +170,17 @@ class HistoryView extends StatelessWidget {
|
||||||
trailing: Wrap(children: [
|
trailing: Wrap(children: [
|
||||||
openInBrowserButton,
|
openInBrowserButton,
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
|
icon: const Icon(Icons.share,
|
||||||
|
color: blueColor, textDirection: TextDirection.ltr),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Share.share(fullPasteUrl);
|
await Share.share(fullPasteUrl);
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
subtitle: Text(!paste.isMulti! ? paste.filename! : '', style: TextStyle(fontStyle: FontStyle.italic)),
|
subtitle: Text(!paste.isMulti! ? paste.filename! : '',
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic)),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
});
|
}
|
||||||
} else {
|
} else {
|
||||||
cards.add(Card(
|
cards.add(Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
@ -181,14 +191,15 @@ class HistoryView extends StatelessWidget {
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
children: cards,
|
children: cards,
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _renderOpenInBrowser(HistoryModel model, String url) {
|
Widget _renderOpenInBrowser(HistoryModel model, String url) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr),
|
icon: const Icon(Icons.open_in_new,
|
||||||
|
color: blueColor, textDirection: TextDirection.ltr),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
return model.openLink(url);
|
return model.openLink(url);
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,12 +9,16 @@ import 'base_view.dart';
|
||||||
class HomeView extends StatelessWidget {
|
class HomeView extends StatelessWidget {
|
||||||
static const routeName = '/home';
|
static const routeName = '/home';
|
||||||
|
|
||||||
|
const HomeView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BaseView<HomeModel>(
|
return BaseView<HomeModel>(
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('app.title'))),
|
appBar: MyAppBar(title: Text(translate('app.title'))),
|
||||||
body: model.state == ViewState.Busy ? Center(child: CircularProgressIndicator()) : Container()),
|
body: model.state == ViewState.busy
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: Container()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ class LoginView extends StatelessWidget {
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
final DialogService _dialogService = locator<DialogService>();
|
final DialogService _dialogService = locator<DialogService>();
|
||||||
|
|
||||||
|
LoginView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final logo = Hero(
|
final logo = Hero(
|
||||||
|
@ -35,11 +37,11 @@ class LoginView extends StatelessWidget {
|
||||||
onModelReady: (model) => model.init(),
|
onModelReady: (model) => model.init(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
appBar: MyAppBar(title: Text(translate('titles.login'))),
|
appBar: MyAppBar(title: Text(translate('titles.login'))),
|
||||||
body: model.state == ViewState.Busy
|
body: model.state == ViewState.busy
|
||||||
? Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView(
|
: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
padding: EdgeInsets.only(left: 10.0, right: 10.0),
|
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Center(child: logo),
|
Center(child: logo),
|
||||||
|
@ -50,16 +52,21 @@ class LoginView extends StatelessWidget {
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Icon(Icons.help),
|
child: const Icon(Icons.help),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_dialogService.showDialog(
|
_dialogService.showDialog(
|
||||||
title: translate('login.compatibility_dialog.title'),
|
title: translate(
|
||||||
description: translate('login.compatibility_dialog.body'));
|
'login.compatibility_dialog.title'),
|
||||||
|
description: translate(
|
||||||
|
'login.compatibility_dialog.body'));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
InkWell(
|
InkWell(
|
||||||
child:
|
child: Icon(
|
||||||
Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor),
|
model.useCredentialsLogin
|
||||||
|
? Icons.person_outline
|
||||||
|
: Icons.vpn_key,
|
||||||
|
color: blueColor),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
model.toggleLoginMethod();
|
model.toggleLoginMethod();
|
||||||
},
|
},
|
||||||
|
@ -79,12 +86,13 @@ class LoginView extends StatelessWidget {
|
||||||
apiKeyController: model.apiKeyController),
|
apiKeyController: model.apiKeyController),
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(Icons.login, color: blueColor),
|
icon: const Icon(Icons.login, color: blueColor),
|
||||||
label: Text(translate('login.button')),
|
label: Text(translate('login.button')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var loginSuccess = await model.login();
|
var loginSuccess = await model.login();
|
||||||
if (loginSuccess) {
|
if (loginSuccess) {
|
||||||
_navigationService.navigateAndReplaceTo(HomeView.routeName);
|
_navigationService
|
||||||
|
.navigateAndReplaceTo(HomeView.routeName);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,11 +8,14 @@ import '../shared/app_colors.dart';
|
||||||
import 'history_view.dart';
|
import 'history_view.dart';
|
||||||
|
|
||||||
class AuthenticatedNavBarView extends StatefulWidget {
|
class AuthenticatedNavBarView extends StatefulWidget {
|
||||||
|
const AuthenticatedNavBarView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AuthenticatedNavBarState createState() => AuthenticatedNavBarState();
|
AuthenticatedNavBarState createState() => AuthenticatedNavBarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> with SingleTickerProviderStateMixin {
|
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
final Logger _logger = getLogger();
|
final Logger _logger = getLogger();
|
||||||
int _currentTabIndex = 0;
|
int _currentTabIndex = 0;
|
||||||
|
|
||||||
|
@ -52,17 +55,17 @@ class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> with Singl
|
||||||
Container(
|
Container(
|
||||||
color: myColor,
|
color: myColor,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: UploadView(),
|
child: const UploadView(),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: myColor,
|
color: myColor,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: HistoryView(),
|
child: const HistoryView(),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
color: myColor,
|
color: myColor,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: ProfileView(),
|
child: const ProfileView(),
|
||||||
),
|
),
|
||||||
][_currentTabIndex],
|
][_currentTabIndex],
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,19 +15,22 @@ import 'base_view.dart';
|
||||||
class ProfileView extends StatelessWidget {
|
class ProfileView extends StatelessWidget {
|
||||||
static const routeName = '/profile';
|
static const routeName = '/profile';
|
||||||
|
|
||||||
|
const ProfileView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BaseView<ProfileModel>(
|
return BaseView<ProfileModel>(
|
||||||
builder: (context, model, child) =>
|
builder: (context, model, child) => Scaffold(
|
||||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.profile'))), body: _render(model, context)));
|
appBar: MyAppBar(title: Text(translate('titles.profile'))),
|
||||||
|
body: _render(model, context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _render(ProfileModel model, BuildContext context) {
|
Widget _render(ProfileModel model, BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
var url = Provider.of<Session>(context).url;
|
||||||
var apiKey = Provider.of<Session>(context).apiKey;
|
var apiKey = Provider.of<Session>(context).apiKey;
|
||||||
|
|
||||||
return model.state == ViewState.Busy
|
return model.state == ViewState.busy
|
||||||
? Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: ListView(
|
: ListView(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
|
@ -45,25 +48,27 @@ class ProfileView extends StatelessWidget {
|
||||||
child: Linkify(
|
child: Linkify(
|
||||||
onOpen: (link) => model.openLink(link.url),
|
onOpen: (link) => model.openLink(link.url),
|
||||||
text: translate('profile.connection', args: {'url': url}),
|
text: translate('profile.connection', args: {'url': url}),
|
||||||
options: LinkifyOptions(humanize: false),
|
options: const LinkifyOptions(humanize: false),
|
||||||
))),
|
))),
|
||||||
UIHelper.verticalSpaceMedium(),
|
UIHelper.verticalSpaceMedium(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
child: model.configLoading
|
child: ElevatedButton.icon(
|
||||||
? Center(
|
icon: model.configLoading
|
||||||
child: Column(
|
? Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: 24,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
height: 24,
|
||||||
children: [
|
padding: const EdgeInsets.all(2.0),
|
||||||
CircularProgressIndicator(),
|
child: const CircularProgressIndicator(
|
||||||
Text(translate('profile.show_config_loading')),
|
color: blueColor,
|
||||||
],
|
strokeWidth: 3,
|
||||||
))
|
),
|
||||||
: ElevatedButton.icon(
|
)
|
||||||
icon: Icon(Icons.settings, color: blueColor),
|
: const Icon(Icons.settings, color: blueColor),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('profile.show_config'),
|
model.configLoading
|
||||||
|
? translate('profile.show_config_loading')
|
||||||
|
: translate('profile.show_config'),
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await model.showConfig(url);
|
await model.showConfig(url);
|
||||||
|
@ -72,7 +77,7 @@ class ProfileView extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: Icon(Icons.lock, color: orangeColor),
|
icon: const Icon(Icons.lock, color: orangeColor),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('profile.reveal_api_key'),
|
translate('profile.reveal_api_key'),
|
||||||
),
|
),
|
||||||
|
@ -83,7 +88,7 @@ class ProfileView extends StatelessWidget {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
icon: Icon(Icons.exit_to_app, color: redColor),
|
icon: const Icon(Icons.exit_to_app, color: redColor),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('profile.logout'),
|
translate('profile.logout'),
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,20 +7,24 @@ import '../../core/viewmodels/startup_model.dart';
|
||||||
class StartUpView extends StatelessWidget {
|
class StartUpView extends StatelessWidget {
|
||||||
static const routeName = '/';
|
static const routeName = '/';
|
||||||
|
|
||||||
|
const StartUpView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ViewModelBuilder<StartUpViewModel>.reactive(
|
return ViewModelBuilder<StartUpViewModel>.reactive(
|
||||||
viewModelBuilder: () => StartUpViewModel(),
|
viewModelBuilder: () => StartUpViewModel(),
|
||||||
onModelReady: (model) => model.handleStartUpLogic(),
|
onViewModelReady: (model) => model.handleStartUpLogic(),
|
||||||
builder: (context, model, child) => Scaffold(
|
builder: (context, model, child) => Scaffold(
|
||||||
body: model.state == ViewState.Busy
|
body: model.state == ViewState.busy
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
(model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
|
(model.stateMessage!.isNotEmpty
|
||||||
|
? Text(model.stateMessage!)
|
||||||
|
: Container())
|
||||||
]))
|
]))
|
||||||
: Container()));
|
: Container()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,18 @@ import 'package:provider/provider.dart';
|
||||||
import '../../core/models/session.dart';
|
import '../../core/models/session.dart';
|
||||||
|
|
||||||
class TabBarContainerView extends StatelessWidget {
|
class TabBarContainerView extends StatelessWidget {
|
||||||
|
const TabBarContainerView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Session? currentSession = Provider.of<Session?>(context);
|
Session? currentSession = Provider.of<Session?>(context);
|
||||||
bool isAuthenticated = currentSession != null ? currentSession.apiKey.isNotEmpty : false;
|
bool isAuthenticated =
|
||||||
|
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
return AuthenticatedNavBarView();
|
return const AuthenticatedNavBarView();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return LoginView();
|
||||||
child: LoginView(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:clipboard/clipboard.dart';
|
import 'package:clipboard/clipboard.dart';
|
||||||
|
import 'package:fbmobile/core/util/formatter_util.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -14,29 +15,35 @@ import 'base_view.dart';
|
||||||
class UploadView extends StatelessWidget {
|
class UploadView extends StatelessWidget {
|
||||||
static const routeName = '/upload';
|
static const routeName = '/upload';
|
||||||
|
|
||||||
|
const UploadView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BaseView<UploadModel>(
|
return BaseView<UploadModel>(
|
||||||
onModelReady: (model) => model.init(),
|
onModelReady: (model) => model.init(),
|
||||||
builder: (context, model, child) =>
|
builder: (context, model, child) => Scaffold(
|
||||||
Scaffold(appBar: MyAppBar(title: Text(translate('titles.upload'))), body: _render(model, context)));
|
appBar: MyAppBar(title: Text(translate('titles.upload'))),
|
||||||
|
body: _render(model, context)));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isUploadButtonEnabled(UploadModel model) {
|
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) {
|
Widget _render(UploadModel model, BuildContext context) {
|
||||||
var url = Provider.of<Session>(context).url;
|
var url = Provider.of<Session>(context).url;
|
||||||
|
|
||||||
return model.state == ViewState.Busy
|
return model.state == ViewState.busy
|
||||||
? Center(
|
? Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
const CircularProgressIndicator(),
|
||||||
(model.stateMessage != null && model.stateMessage!.isNotEmpty ? Text(model.stateMessage!) : Container())
|
(model.stateMessage != null && model.stateMessage!.isNotEmpty
|
||||||
|
? Text(model.stateMessage!)
|
||||||
|
: Container())
|
||||||
]))
|
]))
|
||||||
: ListView(children: <Widget>[
|
: ListView(children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -50,16 +57,19 @@ class UploadView extends StatelessWidget {
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 7,
|
maxLines: 7,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: Icon(
|
prefixIcon: const Icon(
|
||||||
Icons.text_snippet,
|
Icons.text_snippet,
|
||||||
),
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => model.pasteTextController.clear(),
|
onPressed: () =>
|
||||||
icon: Icon(Icons.clear),
|
model.pasteTextController.clear(),
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
),
|
),
|
||||||
hintText: translate('upload.text_to_be_pasted'),
|
hintText: translate('upload.text_to_be_pasted'),
|
||||||
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
contentPadding: const EdgeInsets.fromLTRB(
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
20.0, 10.0, 20.0, 10.0),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(32.0)),
|
||||||
),
|
),
|
||||||
controller: model.pasteTextController)),
|
controller: model.pasteTextController)),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -75,14 +85,17 @@ class UploadView extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(Icons.file_copy_sharp, color: blueColor),
|
icon: const Icon(Icons.file_copy_sharp,
|
||||||
|
color: blueColor),
|
||||||
onPressed: () => model.openFileExplorer(),
|
onPressed: () => model.openFileExplorer(),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('upload.open_file_explorer'),
|
translate('upload.open_file_explorer'),
|
||||||
)),
|
)),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: Icon(Icons.cancel, color: orangeColor),
|
icon: const Icon(Icons.cancel,
|
||||||
onPressed: model.paths != null && model.paths!.length > 0
|
color: orangeColor),
|
||||||
|
onPressed: model.paths != null &&
|
||||||
|
model.paths!.isNotEmpty
|
||||||
? () => model.clearCachedFiles()
|
? () => model.clearCachedFiles()
|
||||||
: null,
|
: null,
|
||||||
label: Text(
|
label: Text(
|
||||||
|
@ -112,66 +125,107 @@ class UploadView extends StatelessWidget {
|
||||||
onPressed: !_isUploadButtonEnabled(model)
|
onPressed: !_isUploadButtonEnabled(model)
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
Map<String, bool>? items = await model.upload();
|
Map<String, bool>? items =
|
||||||
String? clipboardContent = model.generatePasteLinks(items, url);
|
await model.upload();
|
||||||
|
String? clipboardContent = model
|
||||||
|
.generatePasteLinks(items, url);
|
||||||
|
|
||||||
if (clipboardContent != null && clipboardContent.isNotEmpty) {
|
if (clipboardContent != null &&
|
||||||
FlutterClipboard.copy(clipboardContent).then((value) {
|
clipboardContent.isNotEmpty) {
|
||||||
|
FlutterClipboard.copy(
|
||||||
|
clipboardContent)
|
||||||
|
.then((value) {
|
||||||
final snackBar = SnackBar(
|
final snackBar = SnackBar(
|
||||||
action: SnackBarAction(
|
action: SnackBarAction(
|
||||||
label: translate('upload.dismiss'),
|
label: translate(
|
||||||
|
'upload.dismiss'),
|
||||||
textColor: blueColor,
|
textColor: blueColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
ScaffoldMessenger.of(
|
||||||
|
context)
|
||||||
|
.hideCurrentSnackBar();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
content: Text(translate('upload.uploaded')),
|
content: Text(translate(
|
||||||
duration: Duration(seconds: 10),
|
'upload.uploaded')),
|
||||||
|
duration:
|
||||||
|
const Duration(seconds: 10),
|
||||||
);
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(snackBar);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.upload_rounded, color: greenColor),
|
icon: const Icon(Icons.upload_rounded,
|
||||||
|
color: greenColor),
|
||||||
label: Text(
|
label: Text(
|
||||||
translate('upload.upload'),
|
translate('upload.upload'),
|
||||||
)),
|
)),
|
||||||
])),
|
])),
|
||||||
model.errorMessage != null && model.errorMessage!.isNotEmpty
|
model.errorMessage != null && model.errorMessage!.isNotEmpty
|
||||||
? (Padding(
|
? (Padding(
|
||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
padding:
|
||||||
|
const EdgeInsets.only(top: 10.0, bottom: 10.0),
|
||||||
child: CenteredErrorRow(model.errorMessage)))
|
child: CenteredErrorRow(model.errorMessage)))
|
||||||
: Container(),
|
: Container(),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (BuildContext context) => model.loadingPath
|
builder: (BuildContext context) => model.loadingPath
|
||||||
? Padding(
|
? const Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 10.0),
|
padding: EdgeInsets.only(bottom: 10.0),
|
||||||
child: const CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
)
|
)
|
||||||
: model.paths != null
|
: model.paths != null
|
||||||
? Container(
|
? Container(
|
||||||
padding: const EdgeInsets.only(bottom: 30.0),
|
padding: const EdgeInsets.only(bottom: 20.0),
|
||||||
height: MediaQuery.of(context).size.height * 0.50,
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.50,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
itemCount: model.paths != null && model.paths!.isNotEmpty ? model.paths!.length : 1,
|
itemCount: model.paths != null &&
|
||||||
itemBuilder: (BuildContext context, int index) {
|
model.paths!.isNotEmpty
|
||||||
final bool isMultiPath = 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
|
final String name = (isMultiPath
|
||||||
? model.paths!.map((e) => e.name).toList()[index]
|
? model.paths!
|
||||||
|
.map((e) => e.name)
|
||||||
|
.toList()[index]
|
||||||
: model.fileName ?? '...');
|
: model.fileName ?? '...');
|
||||||
final path = model.paths!.length > 0
|
final size = model.paths!.isNotEmpty
|
||||||
? model.paths!.map((e) => e.path).toList()[index].toString()
|
? model.paths!
|
||||||
|
.map((e) => e.size)
|
||||||
|
.toList()[index]
|
||||||
|
.toString()
|
||||||
|
: '';
|
||||||
|
final path = model.paths!.isNotEmpty
|
||||||
|
? model.paths!
|
||||||
|
.map((e) => e.path)
|
||||||
|
.toList()[index]
|
||||||
|
.toString()
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.clear,
|
||||||
|
color: orangeColor),
|
||||||
|
onPressed: () {
|
||||||
|
model.deleteIntentFile(path);
|
||||||
|
}),
|
||||||
title: Text(
|
title: Text(
|
||||||
name,
|
"$name (${FormatterUtil.formatBytes(int.parse(size), 2)})",
|
||||||
),
|
),
|
||||||
subtitle: Text(path),
|
subtitle: Text(path),
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) => const Divider(),
|
separatorBuilder:
|
||||||
|
(BuildContext context, int index) =>
|
||||||
|
const Divider(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
|
|
|
@ -5,14 +5,14 @@ import '../../locator.dart';
|
||||||
import '../../ui/views/about_view.dart';
|
import '../../ui/views/about_view.dart';
|
||||||
|
|
||||||
class AboutIconButton extends StatelessWidget {
|
class AboutIconButton extends StatelessWidget {
|
||||||
AboutIconButton();
|
AboutIconButton({super.key});
|
||||||
|
|
||||||
final NavigationService _navigationService = locator<NavigationService>();
|
final NavigationService _navigationService = locator<NavigationService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(Icons.help),
|
icon: const Icon(Icons.help),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_navigationService.navigateTo(AboutView.routeName);
|
_navigationService.navigateTo(AboutView.routeName);
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ class CenteredErrorRow extends StatelessWidget {
|
||||||
final Function? retryCallback;
|
final Function? retryCallback;
|
||||||
final String? message;
|
final String? message;
|
||||||
|
|
||||||
CenteredErrorRow(this.message, {this.retryCallback});
|
const CenteredErrorRow(this.message, {super.key, this.retryCallback});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -20,7 +20,10 @@ class CenteredErrorRow extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(child: Center(child: Text(message!, style: TextStyle(color: redColor)))),
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Text(message!,
|
||||||
|
style: const TextStyle(color: redColor)))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
(retryCallback != null
|
(retryCallback != null
|
||||||
|
@ -30,7 +33,7 @@ class CenteredErrorRow extends StatelessWidget {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Center(
|
Center(
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
color: primaryAccentColor,
|
color: primaryAccentColor,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
retryCallback!();
|
retryCallback!();
|
||||||
|
|
|
@ -10,18 +10,25 @@ class LoginApiKeyHeaders extends StatelessWidget {
|
||||||
|
|
||||||
final String? validationMessage;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
|
validationMessage != null
|
||||||
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
? Text(validationMessage!, style: const TextStyle(color: redColor))
|
||||||
|
: Container(),
|
||||||
|
LoginTextField(uriController, translate('login.url_placeholder'),
|
||||||
|
const Icon(Icons.link),
|
||||||
keyboardType: TextInputType.url),
|
keyboardType: TextInputType.url),
|
||||||
LoginTextField(
|
LoginTextField(
|
||||||
apiKeyController,
|
apiKeyController,
|
||||||
translate('login.apikey_placeholder'),
|
translate('login.apikey_placeholder'),
|
||||||
Icon(Icons.vpn_key),
|
const Icon(Icons.vpn_key),
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -11,8 +11,9 @@ class LoginCredentialsHeaders extends StatelessWidget {
|
||||||
|
|
||||||
final String? validationMessage;
|
final String? validationMessage;
|
||||||
|
|
||||||
LoginCredentialsHeaders(
|
const LoginCredentialsHeaders(
|
||||||
{required this.uriController,
|
{super.key,
|
||||||
|
required this.uriController,
|
||||||
required this.usernameController,
|
required this.usernameController,
|
||||||
required this.passwordController,
|
required this.passwordController,
|
||||||
this.validationMessage});
|
this.validationMessage});
|
||||||
|
@ -20,12 +21,17 @@ class LoginCredentialsHeaders extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(children: <Widget>[
|
return Column(children: <Widget>[
|
||||||
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
|
validationMessage != null
|
||||||
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
|
? Text(validationMessage!, style: const TextStyle(color: redColor))
|
||||||
|
: Container(),
|
||||||
|
LoginTextField(uriController, translate('login.url_placeholder'),
|
||||||
|
const Icon(Icons.link),
|
||||||
keyboardType: TextInputType.url),
|
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),
|
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),
|
obscureText: true),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../shared/app_colors.dart';
|
|
||||||
|
|
||||||
class LoginTextField extends StatelessWidget {
|
class LoginTextField extends StatelessWidget {
|
||||||
final TextEditingController controller;
|
final TextEditingController controller;
|
||||||
final String placeHolder;
|
final String placeHolder;
|
||||||
|
@ -9,29 +7,31 @@ class LoginTextField extends StatelessWidget {
|
||||||
final bool obscureText;
|
final bool obscureText;
|
||||||
final Widget prefixIcon;
|
final Widget prefixIcon;
|
||||||
|
|
||||||
LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
|
const LoginTextField(this.controller, this.placeHolder, this.prefixIcon,
|
||||||
{this.keyboardType = TextInputType.text, this.obscureText = false});
|
{super.key,
|
||||||
|
this.keyboardType = TextInputType.text,
|
||||||
|
this.obscureText = false});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 10.0),
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
margin: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0),
|
||||||
height: 50.0,
|
height: 50.0,
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
decoration: BoxDecoration(color: whiteColor, borderRadius: BorderRadius.circular(10.0)),
|
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: () => controller.clear(),
|
onPressed: () => controller.clear(),
|
||||||
icon: Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
),
|
),
|
||||||
prefixIcon: prefixIcon,
|
prefixIcon: prefixIcon,
|
||||||
hintText: placeHolder,
|
hintText: placeHolder,
|
||||||
contentPadding: EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
contentPadding: const EdgeInsets.fromLTRB(20.0, 10.0, 20.0, 10.0),
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
border:
|
||||||
|
OutlineInputBorder(borderRadius: BorderRadius.circular(32.0)),
|
||||||
),
|
),
|
||||||
controller: controller),
|
controller: controller),
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,13 +6,18 @@ class MyAppBar extends AppBar {
|
||||||
static final List<Widget> aboutEnabledWidgets = [AboutIconButton()];
|
static final List<Widget> aboutEnabledWidgets = [AboutIconButton()];
|
||||||
static final List<Widget> aboutDisabledWidgets = [];
|
static final List<Widget> aboutDisabledWidgets = [];
|
||||||
|
|
||||||
MyAppBar({Key? key, required Widget title, List<Widget>? actionWidgets, bool enableAbout = true})
|
MyAppBar(
|
||||||
: super(key: key, title: Row(children: <Widget>[title]), actions: _renderIconButtons(actionWidgets, enableAbout));
|
{super.key,
|
||||||
|
required Widget title,
|
||||||
|
List<Widget>? actionWidgets,
|
||||||
|
bool enableAbout = true})
|
||||||
|
: super(
|
||||||
|
title: Row(children: <Widget>[title]),
|
||||||
|
actions: _renderIconButtons(actionWidgets, enableAbout));
|
||||||
|
|
||||||
static List<Widget> _renderIconButtons(List<Widget>? actionWidgets, bool aboutEnabled) {
|
static List<Widget> _renderIconButtons(
|
||||||
if (actionWidgets == null) {
|
List<Widget>? actionWidgets, bool aboutEnabled) {
|
||||||
actionWidgets = [];
|
actionWidgets ??= [];
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> widgets = [...actionWidgets];
|
List<Widget> widgets = [...actionWidgets];
|
||||||
|
|
||||||
|
|
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.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.5.1+17
|
version: 1.6.4+22
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.5 <3.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
cupertino_icons: 1.0.5
|
cupertino_icons: 1.0.8
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_translate: 4.0.3
|
flutter_translate: 4.1.0
|
||||||
provider: 6.0.5
|
provider: 6.1.2
|
||||||
stacked: 3.0.1
|
stacked: 3.4.3
|
||||||
get_it: 7.2.0
|
get_it: 7.7.0
|
||||||
logger: 1.1.0
|
logger: 2.4.0
|
||||||
shared_preferences: 2.0.15
|
shared_preferences: 2.3.2
|
||||||
http: 0.13.5
|
http: 1.2.2
|
||||||
validators: 3.0.0
|
validators: 3.0.0
|
||||||
flutter_linkify: 5.0.2
|
flutter_linkify: 6.0.0
|
||||||
url_launcher: 6.1.7
|
url_launcher: 6.3.1
|
||||||
expandable: 5.0.1
|
expandable: 5.0.1
|
||||||
share_plus: 6.3.0
|
share_plus: 10.1.1
|
||||||
file_picker: 5.2.4
|
file_picker: 8.1.3
|
||||||
clipboard: 0.1.3
|
clipboard: 0.1.3
|
||||||
receive_sharing_intent: 1.4.5
|
permission_handler: 11.3.1
|
||||||
permission_handler: 10.2.0
|
package_info_plus: 8.1.0
|
||||||
package_info_plus: 3.0.2
|
json_annotation: 4.9.0
|
||||||
json_annotation: 4.7.0
|
dynamic_color: 1.7.0
|
||||||
dynamic_color: 1.5.4
|
intl: 0.19.0
|
||||||
|
path: 1.9.0
|
||||||
|
flutter_sharing_intent: 1.1.1
|
||||||
|
device_info_plus: 11.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
build_runner: 2.3.2
|
build_runner: 2.4.13
|
||||||
built_value_generator: 8.4.2
|
built_value_generator: 8.9.2
|
||||||
json_serializable: 6.5.4
|
json_serializable: 6.8.0
|
||||||
|
flutter_lints: 5.0.0
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://www.dartlang.org/tools/pub/pubspec
|
# 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