Compare commits

..

No commits in common. "master" and "1.5.0+16" have entirely different histories.

89 changed files with 899 additions and 1717 deletions

14
.drone.yml Normal file
View file

@ -0,0 +1,14 @@
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

View file

@ -1,21 +0,0 @@
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
View file

@ -1,7 +1,6 @@
# Miscellaneous
*.class
*.lock
!Gemfile.lock
!pubspec.lock
*.log
*.pyc

View file

@ -1,34 +1,5 @@
# CHANGELOG
## 1.6.4+22 - 2024/11/01
* Dependency updates
* Internal build updates
## 1.6.3+21
* Fixed not receiving share requests from other applications
## 1.6.2+20
* Updated internal dependencies
* Moved progress indicator of _Show Configuration_ into the underlying button
* Bumped Android minSdk to `30` (Android 11)
* Bumped Android targetSdk to `34` (Android 14)
* Fixed permission service not handling Android SDK 33 correctly
* Fixed permission service not being started during application start
## 1.6.1+19
* Updated internal dependencies
## 1.6.0+18
* Fixed input colors in login view when using dark theme
* Added removal of individual files selected for upload
* Added size for individual files selected for upload
* Replaced intent sharing library with `flutter_sharing_intent`
* Added proper linting to project
## 1.5.1+17
* Fixed white background button in AppBar when light theme enabled
* Cleaned up login screen and added icon
## 1.5.0+16
* Switched to Material You defaulting to blue swatch colors respecting dark mode
* Switched to Material You navigation bar and removed unsupported swipe navigation

View file

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

View file

@ -2,8 +2,7 @@
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) and
[IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/de.varakh.fbmobile/).
Available on the [Play Store](https://play.google.com/store/apps/details?id=de.varakh.fbmobile).
The main git repository is hosted at **[https://git.myservermanager.com/varakh/fbmobile](https://git.myservermanager.com/varakh/fbmobile)**.
Other repositories are mirrors and pull requests, issues, and planning are managed there.
@ -95,7 +94,7 @@ profiles. They're stored in a separate git repository and are encrypted.
You need access to the git repository in which those private files reside.
#### Usage / doing the actual release
#### Usage
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
@ -105,37 +104,13 @@ following to build for Android `fastlane android build`.
##### Android
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
```
Use `fastlane android beta` to build and upload a new beta version to the Play Store.
##### iOS
For iOS you need to execute `fastlane ios build` before uploading to testflight with
`fastlane ios beta`.
Probably do the same Ruby/fastlane setup as mentioned under the _Android_ section.
### Release manually (not recommended)
See the following links on how to setup:

View file

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

2
android/.gitignore vendored
View file

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

View file

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

View file

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

View file

@ -4,10 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" 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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<queries>
<intent>

View file

@ -21,31 +21,36 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
@ -65,10 +70,7 @@
</application>
<uses-permission android:name="android.permission.INTERNET" />
<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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<queries>
<intent>

View file

@ -4,10 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" 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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<queries>
<intent>

View file

@ -1,3 +1,14 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
}
}
allprojects {
repositories {
google()
@ -13,6 +24,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
task clean(type: Delete) {
delete rootProject.buildDir
}

View file

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

View file

@ -31,14 +31,6 @@ Build Debug
Build Production
### android build_production_fdroid
```sh
[bundle exec] fastlane android build_production_fdroid
```
Build Production fdroid
### android build
```sh

View file

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

View file

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

View file

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

View file

@ -1,25 +1,15 @@
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
}()
include ':app'
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "${agpVersion}" apply false
id "org.jetbrains.kotlin.android" version "${kotlinVersion}" apply false
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
include ":app"

View file

@ -18,6 +18,12 @@
"about": "About",
"upload": "Upload"
},
"tabs": {
"login": "Login",
"history": "History",
"profile": "Profile",
"upload": "New"
},
"upload": {
"and_or": "and/or",
"open_file_explorer": "Select file(s)...",
@ -39,6 +45,7 @@
"start_services": "Starting services..."
},
"login": {
"help": "Login",
"compatibility_dialog": {
"title": "How to login?",
"body": "A FileBin instance >= 3.5.0 is required. Enter valid user and password or switch to API key login by clicking on the icons right next to this help icon."
@ -126,6 +133,14 @@
"description": "Could not open '{link}'. Please ensure that you have an application installed which handles opening such link types."
}
},
"permission_service": {
"dialog": {
"title": "Storage permission",
"description": "Storage permission should be granted to the app so that it can work properly. Do you want to grant permission or ignore this message permanently in the future?",
"grant": "Grant",
"ignore": "Ignore"
}
},
"dialog": {
"confirm": "OK",
"cancel": "Cancel"

View file

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

View file

@ -18,12 +18,10 @@ import 'ui/shared/app_colors.dart';
import 'ui/views/startup_view.dart';
class MyApp extends StatelessWidget {
static final _defaultLightColorScheme = ColorScheme.fromSwatch(
primarySwatch: myColor, brightness: Brightness.light);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(
primarySwatch: myColor, brightness: Brightness.dark);
static final _defaultLightColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.light);
static final _defaultDarkColorScheme = ColorScheme.fromSwatch(primarySwatch: myColor, brightness: Brightness.dark);
MyApp({super.key}) {
MyApp() {
initializeDateFormatting('en');
}
@ -35,33 +33,26 @@ class MyApp extends StatelessWidget {
state: LocalizationProvider.of(context).state,
child: StreamProvider<RefreshEvent?>(
initialData: null,
create: (context) =>
locator<RefreshService>().refreshEventController.stream,
create: (context) => locator<RefreshService>().refreshEventController.stream,
child: StreamProvider<Session?>(
initialData: Session.initial(),
create: (context) =>
locator<SessionService>().sessionController.stream,
child: LifeCycleManager(child: DynamicColorBuilder(
builder: (lightColorScheme, darkColorScheme) {
create: (context) => locator<SessionService>().sessionController.stream,
child: LifeCycleManager(child: DynamicColorBuilder(builder: (lightColorScheme, darkColorScheme) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: translate('app.title'),
builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
onGenerateRoute: (settings) => MaterialPageRoute(builder: (context) => DialogManager(child: child)),
),
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme:
lightColorScheme ?? _defaultLightColorScheme),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
colorScheme: lightColorScheme ?? _defaultLightColorScheme),
darkTheme: ThemeData(useMaterial3: true, colorScheme: darkColorScheme ?? _defaultDarkColorScheme),
onGenerateRoute: AppRouter.generateRoute,
navigatorKey: locator<NavigationService>().navigationKey,
home: const StartUpView(),
home: StartUpView(),
supportedLocales: localizationDelegate.supportedLocales,
locale: localizationDelegate.currentLocale,
);

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,8 @@ class ServiceException implements Exception {
final ErrorCode code;
final String? message;
ServiceException({this.code = ErrorCode.generalError, this.message = ''});
ServiceException({this.code = ErrorCode.GENERAL_ERROR, this.message = ''});
@override
String toString() {
return "$code: $message";
}

View file

@ -8,14 +8,13 @@ import '../services/dialog_service.dart';
class DialogManager extends StatefulWidget {
final Widget? child;
const DialogManager({super.key, this.child});
DialogManager({Key? key, this.child}) : super(key: key);
@override
_DialogManagerState createState() => _DialogManagerState();
}
class _DialogManagerState extends State<DialogManager> {
final DialogService _dialogService = locator<DialogService>();
DialogService _dialogService = locator<DialogService>();
@override
void initState() {
@ -31,8 +30,7 @@ class _DialogManagerState extends State<DialogManager> {
void _showDialog(DialogRequest request) {
List<Widget> actions = <Widget>[];
if (request.buttonTitleDeny != null &&
request.buttonTitleDeny!.isNotEmpty) {
if (request.buttonTitleDeny != null && request.buttonTitleDeny!.isNotEmpty) {
Widget denyBtn = TextButton(
child: Text(request.buttonTitleDeny!),
onPressed: () {

View file

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

View file

@ -15,11 +15,7 @@ class ApiKey {
final String? comment;
ApiKey(
{required this.key,
required this.created,
required this.accessLevel,
this.comment});
ApiKey({required this.key, required this.created, required this.accessLevel, this.comment});
// JSON Init
factory ApiKey.fromJson(Map<String, dynamic> json) => _$ApiKeyFromJson(json);

View file

@ -12,8 +12,7 @@ class ApiKeys {
ApiKeys({required this.apikeys});
// JSON Init
factory ApiKeys.fromJson(Map<String, dynamic> json) =>
_$ApiKeysFromJson(json);
factory ApiKeys.fromJson(Map<String, dynamic> json) => _$ApiKeysFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysToJson(this);

View file

@ -15,8 +15,7 @@ class ApiKeysResponse {
ApiKeysResponse({required this.status, required this.data});
// JSON Init
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) =>
_$ApiKeysResponseFromJson(json);
factory ApiKeysResponse.fromJson(Map<String, dynamic> json) => _$ApiKeysResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ApiKeysResponseToJson(this);

View file

@ -15,8 +15,7 @@ class ConfigResponse {
ConfigResponse({required this.status, required this.data});
// JSON Init
factory ConfigResponse.fromJson(Map<String, dynamic> json) =>
_$ConfigResponseFromJson(json);
factory ConfigResponse.fromJson(Map<String, dynamic> json) => _$ConfigResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$ConfigResponseToJson(this);

View file

@ -13,8 +13,7 @@ class CreateApiKeyResponse {
CreateApiKeyResponse({required this.status, required this.data});
// JSON Init
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) =>
_$CreateApiKeyResponseFromJson(json);
factory CreateApiKeyResponse.fromJson(Map<String, dynamic> json) => _$CreateApiKeyResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$CreateApiKeyResponseToJson(this);

View file

@ -19,8 +19,7 @@ class History {
History({required this.items, required this.multipasteItems, this.totalSize});
// JSON Init
factory History.fromJson(Map<String, dynamic> json) =>
_$HistoryFromJson(json);
factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryToJson(this);

View file

@ -23,8 +23,7 @@ class HistoryItem {
this.thumbnail});
// JSON Init
factory HistoryItem.fromJson(Map<String, dynamic> json) =>
_$HistoryItemFromJson(json);
factory HistoryItem.fromJson(Map<String, dynamic> json) => _$HistoryItemFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryItemToJson(this);

View file

@ -15,8 +15,7 @@ class HistoryMultipasteItem {
HistoryMultipasteItem(this.items, {required this.date, required this.urlId});
// JSON Init
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemFromJson(json);
factory HistoryMultipasteItem.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemToJson(this);

View file

@ -9,8 +9,7 @@ class HistoryMultipasteItemEntry {
HistoryMultipasteItemEntry({required this.id});
// JSON Init
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) =>
_$HistoryMultipasteItemEntryFromJson(json);
factory HistoryMultipasteItemEntry.fromJson(Map<String, dynamic> json) => _$HistoryMultipasteItemEntryFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryMultipasteItemEntryToJson(this);

View file

@ -15,8 +15,7 @@ class HistoryResponse {
HistoryResponse({required this.status, required this.data});
// JSON Init
factory HistoryResponse.fromJson(Map<String, dynamic> json) =>
_$HistoryResponseFromJson(json);
factory HistoryResponse.fromJson(Map<String, dynamic> json) => _$HistoryResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$HistoryResponseToJson(this);

View file

@ -15,8 +15,7 @@ class RestError {
required this.errorId,
}); // JSON Init
factory RestError.fromJson(Map<String, dynamic> json) =>
_$RestErrorFromJson(json);
factory RestError.fromJson(Map<String, dynamic> json) => _$RestErrorFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$RestErrorToJson(this);

View file

@ -13,8 +13,7 @@ class Uploaded {
Uploaded({required this.ids, required this.urls});
// JSON Init
factory Uploaded.fromJson(Map<String, dynamic> json) =>
_$UploadedFromJson(json);
factory Uploaded.fromJson(Map<String, dynamic> json) => _$UploadedFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedToJson(this);

View file

@ -13,8 +13,7 @@ class UploadedMulti {
UploadedMulti({required this.url, required this.urlId});
// JSON Init
factory UploadedMulti.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiFromJson(json);
factory UploadedMulti.fromJson(Map<String, dynamic> json) => _$UploadedMultiFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiToJson(this);

View file

@ -15,8 +15,7 @@ class UploadedMultiResponse {
UploadedMultiResponse({required this.status, required this.data});
// JSON Init
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedMultiResponseFromJson(json);
factory UploadedMultiResponse.fromJson(Map<String, dynamic> json) => _$UploadedMultiResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedMultiResponseToJson(this);

View file

@ -15,8 +15,7 @@ class UploadedResponse {
UploadedResponse({required this.status, required this.data});
// JSON Init
factory UploadedResponse.fromJson(Map<String, dynamic> json) =>
_$UploadedResponseFromJson(json);
factory UploadedResponse.fromJson(Map<String, dynamic> json) => _$UploadedResponseFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedResponseToJson(this);

View file

@ -13,8 +13,7 @@ class Session {
: url = '',
apiKey = '';
factory Session.fromJson(Map<String, dynamic> json) =>
_$SessionFromJson(json);
factory Session.fromJson(Map<String, dynamic> json) => _$SessionFromJson(json);
Map<String, dynamic> toJson() => _$SessionToJson(this);
}

View file

@ -26,8 +26,7 @@ class UploadedPaste {
this.items});
// JSON Init
factory UploadedPaste.fromJson(Map<String, dynamic> json) =>
_$UploadedPasteFromJson(json);
factory UploadedPaste.fromJson(Map<String, dynamic> json) => _$UploadedPasteFromJson(json);
// JSON Export
Map<String, dynamic> toJson() => _$UploadedPasteToJson(this);

View file

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

View file

@ -6,10 +6,10 @@ import '../models/rest/create_apikey_response.dart';
import '../services/api.dart';
class UserRepository {
final Api _api = locator<Api>();
Api _api = locator<Api>();
Future<CreateApiKeyResponse> postApiKey(String url, String username,
String password, String accessLevel, String comment) async {
Future<CreateApiKeyResponse> postApiKey(
String url, String username, String password, String accessLevel, String comment) async {
_api.setUrl(url);
var fields = Map.fromEntries([

View file

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

View file

@ -7,8 +7,7 @@ import '../datamodels/dialog_request.dart';
import '../datamodels/dialog_response.dart';
class DialogService {
final GlobalKey<NavigatorState> _dialogNavigationKey =
GlobalKey<NavigatorState>();
GlobalKey<NavigatorState> _dialogNavigationKey = GlobalKey<NavigatorState>();
late Function(DialogRequest) _showDialogListener;
Completer<DialogResponse>? _dialogCompleter;
@ -28,28 +27,20 @@ class DialogService {
title: title,
description: description,
buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty
? translate('dialog.confirm')
: buttonTitleAccept));
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept));
return _dialogCompleter!.future;
}
Future<DialogResponse> showConfirmationDialog(
{String? title,
String? description,
String? buttonTitleAccept,
String? buttonTitleDeny}) {
{String? title, String? description, String? buttonTitleAccept, String? buttonTitleDeny}) {
_dialogCompleter = Completer<DialogResponse>();
_showDialogListener(DialogRequest(
title: title,
description: description,
buttonTitleAccept:
buttonTitleAccept == null || buttonTitleAccept.isEmpty
? translate('dialog.confirm')
: buttonTitleAccept,
buttonTitleDeny: buttonTitleDeny == null || buttonTitleDeny.isEmpty
? translate('dialog.cancel')
: buttonTitleDeny));
buttonTitleAccept == null || buttonTitleAccept.isEmpty ? translate('dialog.confirm') : buttonTitleAccept,
buttonTitleDeny:
buttonTitleDeny == null || buttonTitleDeny.isEmpty ? translate('dialog.cancel') : buttonTitleDeny));
return _dialogCompleter!.future;
}

View file

@ -23,8 +23,7 @@ class FileService {
return await _fileRepository.postDelete(id);
}
Future<UploadedResponse> uploadPaste(
List<File>? files, Map<String, String>? additionalFiles) async {
Future<UploadedResponse> uploadPaste(List<File>? files, Map<String, String>? additionalFiles) async {
return await _fileRepository.postUpload(files, additionalFiles);
}

View file

@ -19,8 +19,7 @@ class LinkService {
_logger.e('Could not launch link $link');
_dialogService.showDialog(
title: translate('link.dialog.title'),
description:
translate('link.dialog.description', args: {'link': link}));
description: translate('link.dialog.description', args: {'link': link}));
}
}
}

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class SessionService extends StoppableService {
Future<bool> login(String url, String apiKey) async {
setApiConfig(url, apiKey);
var session = Session(url: url, apiKey: apiKey);
var session = new Session(url: url, apiKey: apiKey);
sessionController.add(session);
await _storageService.storeSession(session);
_logger.d('Session created');

View file

@ -5,36 +5,45 @@ import 'package:shared_preferences/shared_preferences.dart';
import '../models/session.dart';
class StorageService {
static const _sessionKey = 'session';
static const _lastUrlKey = 'last_url';
static const _SESSION_KEY = 'session';
static const _LAST_URL_KEY = 'last_url';
static const _STORAGE_PERMISSION_DIALOG_IGNORED = 'storage_permission_ignored';
Future<bool> storeLastUrl(String url) {
return _store(_lastUrlKey, url);
return _store(_LAST_URL_KEY, url);
}
Future<String?> retrieveLastUrl() async {
return await _retrieve(_lastUrlKey);
return await _retrieve(_LAST_URL_KEY);
}
Future<bool> hasLastUrl() async {
return await _exists(_lastUrlKey);
return await _exists(_LAST_URL_KEY);
}
Future<bool> storeSession(Session session) {
return _store(_sessionKey, json.encode(session));
return _store(_SESSION_KEY, json.encode(session));
}
Future<Session> retrieveSession() async {
var retrieve = await _retrieve(_sessionKey);
var retrieve = await _retrieve(_SESSION_KEY);
return Session.fromJson(json.decode(retrieve!));
}
Future<bool> hasSession() {
return _exists(_sessionKey);
return _exists(_SESSION_KEY);
}
Future<bool> removeSession() {
return _remove(_sessionKey);
return _remove(_SESSION_KEY);
}
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 {

View file

@ -12,10 +12,9 @@ class UserService {
final FileService _fileService = locator<FileService>();
final UserRepository _userRepository = locator<UserRepository>();
Future<CreateApiKeyResponse> createApiKey(String url, String username,
String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey(
url, username, password, accessLevel, comment);
Future<CreateApiKeyResponse> createApiKey(
String url, String username, String password, String accessLevel, String comment) async {
return await _userRepository.postApiKey(url, username, password, accessLevel, comment);
}
Future<ApiKeysResponse> getApiKeys() async {
@ -27,7 +26,7 @@ class UserService {
try {
await _fileService.getHistory();
} on ServiceException catch (e) {
throw ServiceException(code: ErrorCode.invalidApiKey, message: e.message);
throw new ServiceException(code: ErrorCode.INVALID_API_KEY, message: e.message);
}
}
}

View file

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

View file

@ -20,10 +20,10 @@ class AboutModel extends BaseModel {
}
Future<void> _initPackageInfo() async {
setStateView(ViewState.busy);
setStateView(ViewState.Busy);
final PackageInfo info = await PackageInfo.fromPlatform();
packageInfo = info;
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
}
void openLink(String link) {

View file

@ -5,21 +5,18 @@ import '../../core/util/logger.dart';
import '../enums/viewstate.dart';
class BaseModel extends ChangeNotifier {
static const String stateViewKey = 'viewState';
static const String stateMessageKey = 'viewMessage';
static const String STATE_VIEW = 'viewState';
static const String STATE_MESSAGE = 'viewMessage';
final Logger _logger = getLogger();
bool _isDisposed = false;
final Map<String, Object?> _stateMap = {
stateViewKey: ViewState.idle,
stateMessageKey: null
};
Map<String, Object?> _stateMap = {STATE_VIEW: ViewState.Idle, STATE_MESSAGE: null};
ViewState? get state => _stateMap[stateViewKey] as ViewState?;
ViewState? get state => _stateMap[STATE_VIEW] as ViewState?;
String? get stateMessage => _stateMap[stateMessageKey] as String?;
String? get stateMessage => _stateMap[STATE_MESSAGE] as String?;
bool getStateValueAsBoolean(String key) {
if (_stateMap.containsKey(key) && _stateMap[key] is bool) {
@ -45,14 +42,11 @@ class BaseModel extends ChangeNotifier {
return null;
}
void setStateBoolValue(String key, bool stateValue) =>
_setStateValue(key, stateValue);
void setStateBoolValue(String key, bool stateValue) => _setStateValue(key, stateValue);
void setStateIntValue(String key, int? stateValue) =>
_setStateValue(key, stateValue);
void setStateIntValue(String key, int? stateValue) => _setStateValue(key, stateValue);
void setStateStringValue(String key, String? stateValue) =>
_setStateValue(key, stateValue);
void setStateStringValue(String key, String? stateValue) => _setStateValue(key, stateValue);
void _setStateValue(String key, Object? stateValue) {
if (_stateMap.containsKey(key)) {
@ -63,8 +57,7 @@ class BaseModel extends ChangeNotifier {
if (!_isDisposed) {
notifyListeners();
_logger
.d("Notified state value update '($key, ${stateValue.toString()})'");
_logger.d("Notified state value update '($key, ${stateValue.toString()})'");
}
}
@ -78,16 +71,15 @@ class BaseModel extends ChangeNotifier {
}
void setStateView(ViewState stateView) {
_setStateValue(stateViewKey, stateView);
_setStateValue(STATE_VIEW, stateView);
}
void setStateMessage(String? stateMessage) {
_setStateValue(stateMessageKey, stateMessage);
_setStateValue(STATE_MESSAGE, stateMessage);
}
@override
void dispose() {
_logger.d("Calling dispose");
super.dispose();
_isDisposed = true;
}

View file

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

View file

@ -20,12 +20,12 @@ import '../util/logger.dart';
import 'base_model.dart';
class LoginModel extends BaseModel {
TextEditingController _uriController = TextEditingController();
TextEditingController _uriController = new TextEditingController();
final TextEditingController _userNameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _userNameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController();
final TextEditingController _apiKeyController = TextEditingController();
final TextEditingController _apiKeyController = new TextEditingController();
TextEditingController get uriController => _uriController;
@ -44,23 +44,23 @@ class LoginModel extends BaseModel {
String? errorMessage;
void toggleLoginMethod() {
setStateView(ViewState.busy);
setStateView(ViewState.Busy);
useCredentialsLogin = !useCredentialsLogin;
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
}
void init() async {
bool hasLastUrl = await _storageService.hasLastUrl();
if (hasLastUrl) {
setStateView(ViewState.busy);
setStateView(ViewState.Busy);
var s = await (_storageService.retrieveLastUrl() as FutureOr<String>);
if (s.isNotEmpty) {
_uriController = TextEditingController(text: s);
_uriController = new TextEditingController(text: s);
}
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
}
}
@ -70,45 +70,45 @@ class LoginModel extends BaseModel {
var password = passwordController.text;
var apiKey = apiKeyController.text;
setStateView(ViewState.busy);
setStateView(ViewState.Busy);
url = trim(url);
username = trim(username);
if (url.isEmpty) {
errorMessage = translate('login.errors.empty_url');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
if (!url.contains("https://") && !url.contains("http://")) {
errorMessage = translate('login.errors.no_protocol');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
bool validUri = Uri.parse(url).isAbsolute;
if (!validUri || !isURL(url)) {
errorMessage = translate('login.errors.invalid_url');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
if (useCredentialsLogin) {
if (username.isEmpty) {
errorMessage = translate('login.errors.empty_username');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
if (password.isEmpty) {
errorMessage = translate('login.errors.empty_password');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
} else {
if (apiKey.isEmpty) {
errorMessage = translate('login.errors.empty_apikey');
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return false;
}
}
@ -117,19 +117,14 @@ class LoginModel extends BaseModel {
try {
if (useCredentialsLogin) {
CreateApiKeyResponse apiKeyResponse = await _userService.createApiKey(
url,
username,
password,
'apikey',
'fbmobile-${DateTime.now().millisecondsSinceEpoch}');
url, username, password, 'apikey', 'fbmobile-${new DateTime.now().millisecondsSinceEpoch}');
var newKey = apiKeyResponse.data['new_key'];
if (newKey != null) {
success = await _sessionService.login(url, newKey);
} else {
throw ServiceException(
code: ErrorCode.invalidApiKey,
message: translate('login.errors.invalid_api_key'));
throw new ServiceException(
code: ErrorCode.INVALID_API_KEY, message: translate('login.errors.invalid_api_key'));
}
} else {
_sessionService.setApiConfig(url, apiKey);
@ -141,37 +136,35 @@ class LoginModel extends BaseModel {
if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found');
}
if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
} else {
errorMessage = translate('api.general_rest_error');
}
} else if (e is ServiceException && e.code == ErrorCode.invalidApiKey) {
} else if (e is ServiceException && e.code == ErrorCode.INVALID_API_KEY) {
errorMessage = translate('login.errors.invalid_api_key');
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
errorMessage = translate('api.socket_timeout');
} else {
errorMessage = translate('app.unknown_error');
_sessionService.logout();
setStateView(ViewState.idle);
_logger.e('An unknown error occurred', error: e);
rethrow;
setStateView(ViewState.Idle);
_logger.e('An unknown error occurred', e);
throw e;
}
if (errorMessage!.isNotEmpty) {
_sessionService.logout();
}
setStateView(ViewState.idle);
setStateView(ViewState.Idle);
return success;
}

View file

@ -31,8 +31,7 @@ class ProfileModel extends BaseModel {
Future logout() async {
var dialogResult = await _dialogService.showConfirmationDialog(
title: translate('logout.title'),
description: translate('logout.confirm'));
title: translate('logout.title'), description: translate('logout.confirm'));
if (dialogResult.confirmed!) {
await _sessionService.logout();
@ -42,8 +41,7 @@ class ProfileModel extends BaseModel {
Future revealApiKey(String? apiKey) async {
await _dialogService.showDialog(
title: translate('profile.revealed_api_key.title'),
description: translate('profile.revealed_api_key.description',
args: {'apiKey': apiKey}));
description: translate('profile.revealed_api_key.description', args: {'apiKey': apiKey}));
}
Future showConfig(String url) async {
@ -56,29 +54,27 @@ class ProfileModel extends BaseModel {
if (e is RestServiceException) {
if (e.statusCode == HttpStatus.unauthorized) {
errorMessage = translate('login.errors.wrong_credentials');
} else if (e.statusCode != HttpStatus.unauthorized &&
e.statusCode == HttpStatus.forbidden) {
} else if (e.statusCode != HttpStatus.unauthorized && e.statusCode == HttpStatus.forbidden) {
errorMessage = translate('login.errors.forbidden');
} else if (e.statusCode == HttpStatus.notFound) {
errorMessage = translate('api.incompatible_error_not_found');
}
if (e.statusCode == HttpStatus.badRequest) {
errorMessage = translate('api.bad_request',
args: {'reason': e.responseBody.message});
errorMessage = translate('api.bad_request', args: {'reason': e.responseBody.message});
} else {
errorMessage = translate('api.general_rest_error');
}
} else if (e is ServiceException && e.code == ErrorCode.socketError) {
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_ERROR) {
errorMessage = translate('api.socket_error');
} else if (e is ServiceException && e.code == ErrorCode.socketTimeout) {
} else if (e is ServiceException && e.code == ErrorCode.SOCKET_TIMEOUT) {
errorMessage = translate('api.socket_timeout');
} else {
errorMessage = translate('app.unknown_error');
setStateBoolValue(_configurationButtonLoading, false);
_sessionService.logout();
setStateBoolValue(_configurationButtonLoading, false);
_logger.e('An unknown error occurred', error: e);
rethrow;
_logger.e('An unknown error occurred', e);
throw e;
}
}
@ -88,18 +84,15 @@ class ProfileModel extends BaseModel {
await _dialogService.showDialog(
title: translate('profile.shown_config.title'),
description: translate('profile.shown_config.description', args: {
'uploadMaxSize':
FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'uploadMaxSize': FormatterUtil.formatBytes(config.uploadMaxSize as int, 2),
'maxFilesPerRequest': config.maxFilesPerRequest,
'maxInputVars': config.maxInputVars,
'requestMaxSize':
FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
'requestMaxSize': FormatterUtil.formatBytes(config.requestMaxSize as int, 2)
}));
} else {
await _dialogService.showDialog(
title: translate('profile.shown_config.error.title'),
description: translate('profile.shown_config.error.description',
args: {'message': errorMessage}));
description: translate('profile.shown_config.error.description', args: {'message': errorMessage}));
}
}

View file

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

View file

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

View file

@ -11,8 +11,7 @@ void main() async {
setupLogger(Level.info);
setupLocator();
var delegate = await LocalizationDelegate.create(
fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
var delegate = await LocalizationDelegate.create(fallbackLocale: 'en', supportedLocales: ['en', 'en_US']);
WidgetsFlutterBinding.ensureInitialized();
runApp(LocalizedApp(delegate, MyApp()));

View file

@ -14,21 +14,20 @@ class AppRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case StartUpView.routeName:
return MaterialPageRoute(builder: (_) => const StartUpView());
return MaterialPageRoute(builder: (_) => StartUpView());
case AboutView.routeName:
return MaterialPageRoute(builder: (_) => const AboutView());
return MaterialPageRoute(builder: (_) => AboutView());
case HomeView.routeName:
return MaterialPageRoute(builder: (_) => const TabBarContainerView());
return MaterialPageRoute(builder: (_) => TabBarContainerView());
case LoginView.routeName:
return MaterialPageRoute(builder: (_) => LoginView());
case ProfileView.routeName:
return MaterialPageRoute(builder: (_) => const ProfileView());
return MaterialPageRoute(builder: (_) => ProfileView());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text(translate('dev.no_route',
args: {'route': settings.name})),
child: Text(translate('dev.no_route', args: {'route': settings.name})),
),
));
}

View file

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

View file

@ -12,8 +12,6 @@ import 'base_view.dart';
class AboutView extends StatelessWidget {
static const routeName = '/about';
const AboutView({super.key});
@override
Widget build(BuildContext context) {
final logo = Hero(
@ -32,14 +30,13 @@ class AboutView extends StatelessWidget {
title: Text(translate('titles.about')),
enableAbout: false,
),
body: model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
body: model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: Container(
padding: const EdgeInsets.all(0),
padding: EdgeInsets.all(0),
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.only(
left: 10.0, right: 10.0, bottom: 10, top: 10),
padding: EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10, top: 10),
children: <Widget>[
UIHelper.verticalSpaceMedium(),
Center(child: logo),
@ -78,7 +75,7 @@ class AboutView extends StatelessWidget {
Center(
child: Linkify(
text: translate('about.website'),
options: const LinkifyOptions(humanize: false),
options: LinkifyOptions(humanize: false),
onOpen: (link) => model.openLink(link.url),
),
)

View file

@ -10,7 +10,7 @@ class BaseView<T extends BaseModel> extends StatefulWidget {
final Widget Function(BuildContext context, T model, Widget? child)? builder;
final Function(T)? onModelReady;
const BaseView({super.key, this.builder, this.onModelReady});
BaseView({this.builder, this.onModelReady});
@override
_BaseViewState<T> createState() => _BaseViewState<T>();
@ -31,9 +31,7 @@ class _BaseViewState<T extends BaseModel> extends State<BaseView<T>> {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<T?>(
create: (context) => model,
child: Consumer<T>(builder: widget.builder!));
return ChangeNotifierProvider<T?>(create: (context) => model, child: Consumer<T>(builder: widget.builder!));
}
@override

View file

@ -18,8 +18,6 @@ import 'base_view.dart';
class HistoryView extends StatelessWidget {
static const routeName = '/history';
const HistoryView({super.key});
@override
Widget build(BuildContext context) {
return BaseView<HistoryModel>(
@ -27,25 +25,23 @@ class HistoryView extends StatelessWidget {
model.init();
return model.getHistory();
},
builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.history'))),
body: _render(model, context)),
builder: (context, model, child) =>
Scaffold(appBar: MyAppBar(title: Text(translate('titles.history'))), body: _render(model, context)),
);
}
Widget _render(HistoryModel model, BuildContext context) {
var url = Provider.of<Session>(context).url;
return model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
return model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: (model.errorMessage == null
? Container(
padding: const EdgeInsets.all(0),
padding: EdgeInsets.all(0),
child: RefreshIndicator(
onRefresh: () async => await model.getHistory(),
child: _renderItems(model, url, context)))
onRefresh: () async => await model.getHistory(), child: _renderItems(model, url, context)))
: Container(
padding: const EdgeInsets.all(25),
padding: EdgeInsets.all(25),
child: CenteredErrorRow(
model.errorMessage,
retryCallback: () => model.getHistory(),
@ -55,16 +51,15 @@ class HistoryView extends StatelessWidget {
Widget _renderItems(HistoryModel model, String url, BuildContext context) {
List<Widget> cards = [];
if (model.pastes.isNotEmpty) {
for (var paste in model.pastes.reversed) {
if (model.pastes.length > 0) {
model.pastes.reversed.forEach((paste) {
List<Widget> widgets = [];
var fullPasteUrl = PasteUtil.generateLink(url, paste.id);
var openInBrowserButton = _renderOpenInBrowser(model, fullPasteUrl);
var dateWidget = ListTile(
title: Text(
FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
title: Text(FormatterUtil.formatEpoch(paste.date!.millisecondsSinceEpoch)),
subtitle: Text(translate('history.date')),
);
@ -76,8 +71,7 @@ class HistoryView extends StatelessWidget {
var copyWidget = ListTile(
title: Text(translate('history.copy_link.description')),
trailing: IconButton(
icon: const Icon(Icons.copy,
color: blueColor, textDirection: TextDirection.ltr),
icon: Icon(Icons.copy, color: blueColor, textDirection: TextDirection.ltr),
onPressed: () {
FlutterClipboard.copy(fullPasteUrl).then((value) {
final snackBar = SnackBar(
@ -89,18 +83,16 @@ class HistoryView extends StatelessWidget {
},
),
content: Text(translate('history.copy_link.copied')),
duration: const Duration(seconds: 10),
duration: Duration(seconds: 10),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}));
var deleteWidget = ListTile(
title: Text(translate('history.delete')),
trailing: IconButton(
icon: const Icon(Icons.delete, color: redColor),
icon: Icon(Icons.delete, color: redColor),
onPressed: () {
model.deletePaste(paste.id);
}));
@ -128,13 +120,13 @@ class HistoryView extends StatelessWidget {
widgets.add(fileSizeWidget);
widgets.add(mimeTypeWidget);
} else {
for (var element in paste.items!) {
paste.items!.forEach((element) {
widgets.add(ListTile(
title: Text(element!),
subtitle: Text(translate('history.multipaste_element')),
trailing: _renderOpenInBrowser(model, '$url/$element'),
));
}
});
}
widgets.add(dateWidget);
@ -143,7 +135,7 @@ class HistoryView extends StatelessWidget {
widgets.add(deleteWidget);
var expandable = ExpandableTheme(
data: const ExpandableThemeData(
data: ExpandableThemeData(
iconPlacement: ExpandablePanelIconPlacement.right,
headerAlignment: ExpandablePanelHeaderAlignment.center,
hasIcon: true,
@ -154,7 +146,7 @@ class HistoryView extends StatelessWidget {
header: InkWell(
child: Text(
paste.id,
style: const TextStyle(color: blueColor),
style: TextStyle(color: blueColor),
textAlign: TextAlign.left,
)),
expanded: Column(
@ -170,17 +162,15 @@ class HistoryView extends StatelessWidget {
trailing: Wrap(children: [
openInBrowserButton,
IconButton(
icon: const Icon(Icons.share,
color: blueColor, textDirection: TextDirection.ltr),
icon: Icon(Icons.share, color: blueColor, textDirection: TextDirection.ltr),
onPressed: () async {
await Share.share(fullPasteUrl);
})
]),
subtitle: Text(!paste.isMulti! ? paste.filename! : '',
style: const TextStyle(fontStyle: FontStyle.italic)),
subtitle: Text(!paste.isMulti! ? paste.filename! : '', style: TextStyle(fontStyle: FontStyle.italic)),
),
));
}
});
} else {
cards.add(Card(
child: ListTile(
@ -191,15 +181,14 @@ class HistoryView extends StatelessWidget {
return ListView(
padding: const EdgeInsets.all(8),
physics: const AlwaysScrollableScrollPhysics(),
children: cards,
physics: AlwaysScrollableScrollPhysics(),
);
}
Widget _renderOpenInBrowser(HistoryModel model, String url) {
return IconButton(
icon: const Icon(Icons.open_in_new,
color: blueColor, textDirection: TextDirection.ltr),
icon: Icon(Icons.open_in_new, color: blueColor, textDirection: TextDirection.ltr),
onPressed: () {
return model.openLink(url);
});

View file

@ -9,16 +9,12 @@ import 'base_view.dart';
class HomeView extends StatelessWidget {
static const routeName = '/home';
const HomeView({super.key});
@override
Widget build(BuildContext context) {
return BaseView<HomeModel>(
builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('app.title'))),
body: model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
: Container()),
body: model.state == ViewState.Busy ? Center(child: CircularProgressIndicator()) : Container()),
);
}
}

View file

@ -6,6 +6,7 @@ import '../../core/services/dialog_service.dart';
import '../../core/services/navigation_service.dart';
import '../../core/viewmodels/login_model.dart';
import '../../locator.dart';
import '../../ui/shared/text_styles.dart';
import '../../ui/views/home_view.dart';
import '../../ui/widgets/my_appbar.dart';
import '../shared/app_colors.dart';
@ -20,8 +21,6 @@ class LoginView extends StatelessWidget {
final NavigationService _navigationService = locator<NavigationService>();
final DialogService _dialogService = locator<DialogService>();
LoginView({super.key});
@override
Widget build(BuildContext context) {
final logo = Hero(
@ -37,11 +36,11 @@ class LoginView extends StatelessWidget {
onModelReady: (model) => model.init(),
builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.login'))),
body: model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
body: model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: ListView(
shrinkWrap: true,
padding: const EdgeInsets.only(left: 10.0, right: 10.0),
padding: EdgeInsets.only(left: 10.0, right: 10.0),
children: <Widget>[
UIHelper.verticalSpaceMedium(),
Center(child: logo),
@ -51,22 +50,21 @@ class LoginView extends StatelessWidget {
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.center,
children: <Widget>[
Text(
translate('login.help'),
style: subHeaderStyle,
),
InkWell(
child: const Icon(Icons.help),
child: Icon(Icons.help),
onTap: () {
_dialogService.showDialog(
title: translate(
'login.compatibility_dialog.title'),
description: translate(
'login.compatibility_dialog.body'));
title: translate('login.compatibility_dialog.title'),
description: translate('login.compatibility_dialog.body'));
},
),
InkWell(
child: Icon(
model.useCredentialsLogin
? Icons.person_outline
: Icons.vpn_key,
color: blueColor),
child:
Icon(model.useCredentialsLogin ? Icons.person_outline : Icons.vpn_key, color: blueColor),
onTap: () {
model.toggleLoginMethod();
},
@ -85,14 +83,12 @@ class LoginView extends StatelessWidget {
uriController: model.uriController,
apiKeyController: model.apiKeyController),
UIHelper.verticalSpaceMedium(),
ElevatedButton.icon(
icon: const Icon(Icons.login, color: blueColor),
label: Text(translate('login.button')),
ElevatedButton(
child: Text(translate('login.button')),
onPressed: () async {
var loginSuccess = await model.login();
if (loginSuccess) {
_navigationService
.navigateAndReplaceTo(HomeView.routeName);
_navigationService.navigateAndReplaceTo(HomeView.routeName);
}
},
)

View file

@ -8,14 +8,11 @@ import '../shared/app_colors.dart';
import 'history_view.dart';
class AuthenticatedNavBarView extends StatefulWidget {
const AuthenticatedNavBarView({super.key});
@override
AuthenticatedNavBarState createState() => AuthenticatedNavBarState();
}
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView>
with SingleTickerProviderStateMixin {
class AuthenticatedNavBarState extends State<AuthenticatedNavBarView> with SingleTickerProviderStateMixin {
final Logger _logger = getLogger();
int _currentTabIndex = 0;
@ -55,17 +52,17 @@ class AuthenticatedNavBarState extends State<AuthenticatedNavBarView>
Container(
color: myColor,
alignment: Alignment.center,
child: const UploadView(),
child: UploadView(),
),
Container(
color: myColor,
alignment: Alignment.center,
child: const HistoryView(),
child: HistoryView(),
),
Container(
color: myColor,
alignment: Alignment.center,
child: const ProfileView(),
child: ProfileView(),
),
][_currentTabIndex],
);

View file

@ -15,22 +15,19 @@ import 'base_view.dart';
class ProfileView extends StatelessWidget {
static const routeName = '/profile';
const ProfileView({super.key});
@override
Widget build(BuildContext context) {
return BaseView<ProfileModel>(
builder: (context, model, child) => Scaffold(
appBar: MyAppBar(title: Text(translate('titles.profile'))),
body: _render(model, context)));
builder: (context, model, child) =>
Scaffold(appBar: MyAppBar(title: Text(translate('titles.profile'))), body: _render(model, context)));
}
Widget _render(ProfileModel model, BuildContext context) {
var url = Provider.of<Session>(context).url;
var apiKey = Provider.of<Session>(context).apiKey;
return model.state == ViewState.busy
? const Center(child: CircularProgressIndicator())
return model.state == ViewState.Busy
? Center(child: CircularProgressIndicator())
: ListView(
children: <Widget>[
UIHelper.verticalSpaceMedium(),
@ -48,36 +45,34 @@ class ProfileView extends StatelessWidget {
child: Linkify(
onOpen: (link) => model.openLink(link.url),
text: translate('profile.connection', args: {'url': url}),
options: const LinkifyOptions(humanize: false),
options: LinkifyOptions(humanize: false),
))),
UIHelper.verticalSpaceMedium(),
Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: ElevatedButton.icon(
icon: model.configLoading
? Container(
width: 24,
height: 24,
padding: const EdgeInsets.all(2.0),
child: const CircularProgressIndicator(
color: blueColor,
strokeWidth: 3,
),
)
: const Icon(Icons.settings, color: blueColor),
label: Text(
model.configLoading
? translate('profile.show_config_loading')
: translate('profile.show_config'),
),
onPressed: () async {
await model.showConfig(url);
})),
child: model.configLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator(),
Text(translate('profile.show_config_loading')),
],
))
: ElevatedButton.icon(
icon: Icon(Icons.settings, color: blueColor),
label: Text(
translate('profile.show_config'),
),
onPressed: () async {
await model.showConfig(url);
})),
UIHelper.verticalSpaceMedium(),
Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.lock, color: orangeColor),
icon: Icon(Icons.lock, color: orangeColor),
label: Text(
translate('profile.reveal_api_key'),
),
@ -88,7 +83,7 @@ class ProfileView extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 25.0, right: 25.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.exit_to_app, color: redColor),
icon: Icon(Icons.exit_to_app, color: redColor),
label: Text(
translate('profile.logout'),
),

View file

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

View file

@ -6,18 +6,17 @@ import 'package:provider/provider.dart';
import '../../core/models/session.dart';
class TabBarContainerView extends StatelessWidget {
const TabBarContainerView({super.key});
@override
Widget build(BuildContext context) {
Session? currentSession = Provider.of<Session?>(context);
bool isAuthenticated =
currentSession != null ? currentSession.apiKey.isNotEmpty : false;
bool isAuthenticated = currentSession != null ? currentSession.apiKey.isNotEmpty : false;
if (isAuthenticated) {
return const AuthenticatedNavBarView();
return AuthenticatedNavBarView();
}
return LoginView();
return Container(
child: LoginView(),
);
}
}

View file

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

View file

@ -3,16 +3,18 @@ import 'package:flutter/material.dart';
import '../../core/services/navigation_service.dart';
import '../../locator.dart';
import '../../ui/views/about_view.dart';
import '../shared/app_colors.dart';
class AboutIconButton extends StatelessWidget {
AboutIconButton({super.key});
AboutIconButton();
final NavigationService _navigationService = locator<NavigationService>();
@override
Widget build(BuildContext context) {
return IconButton(
icon: const Icon(Icons.help),
icon: Icon(Icons.help),
color: whiteColor,
onPressed: () {
_navigationService.navigateTo(AboutView.routeName);
});

View file

@ -6,7 +6,7 @@ class CenteredErrorRow extends StatelessWidget {
final Function? retryCallback;
final String? message;
const CenteredErrorRow(this.message, {super.key, this.retryCallback});
CenteredErrorRow(this.message, {this.retryCallback});
@override
Widget build(BuildContext context) {
@ -20,10 +20,7 @@ class CenteredErrorRow extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Center(
child: Text(message!,
style: const TextStyle(color: redColor)))),
Expanded(child: Center(child: Text(message!, style: TextStyle(color: redColor)))),
],
),
(retryCallback != null
@ -33,7 +30,7 @@ class CenteredErrorRow extends StatelessWidget {
children: <Widget>[
Center(
child: IconButton(
icon: const Icon(Icons.refresh),
icon: Icon(Icons.refresh),
color: primaryAccentColor,
onPressed: () {
retryCallback!();

View file

@ -10,25 +10,18 @@ class LoginApiKeyHeaders extends StatelessWidget {
final String? validationMessage;
const LoginApiKeyHeaders(
{super.key,
required this.uriController,
required this.apiKeyController,
this.validationMessage});
LoginApiKeyHeaders({required this.uriController, required this.apiKeyController, this.validationMessage});
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
validationMessage != null
? Text(validationMessage!, style: const TextStyle(color: redColor))
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(
apiKeyController,
translate('login.apikey_placeholder'),
const Icon(Icons.vpn_key),
Icon(Icons.vpn_key),
obscureText: true,
),
]);

View file

@ -11,9 +11,8 @@ class LoginCredentialsHeaders extends StatelessWidget {
final String? validationMessage;
const LoginCredentialsHeaders(
{super.key,
required this.uriController,
LoginCredentialsHeaders(
{required this.uriController,
required this.usernameController,
required this.passwordController,
this.validationMessage});
@ -21,17 +20,12 @@ class LoginCredentialsHeaders extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
validationMessage != null
? Text(validationMessage!, style: const TextStyle(color: redColor))
: Container(),
LoginTextField(uriController, translate('login.url_placeholder'),
const Icon(Icons.link),
this.validationMessage != null ? Text(validationMessage!, style: TextStyle(color: redColor)) : Container(),
LoginTextField(uriController, translate('login.url_placeholder'), Icon(Icons.link),
keyboardType: TextInputType.url),
LoginTextField(usernameController,
translate('login.username_placeholder'), const Icon(Icons.person),
LoginTextField(usernameController, translate('login.username_placeholder'), Icon(Icons.person),
keyboardType: TextInputType.name),
LoginTextField(passwordController,
translate('login.password_placeholder'), const Icon(Icons.vpn_key),
LoginTextField(passwordController, translate('login.password_placeholder'), Icon(Icons.vpn_key),
obscureText: true),
]);
}

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -11,47 +11,43 @@ description: A mobile client for FileBin.
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.6.4+22
version: 1.5.0+16
environment:
sdk: '>=3.5.0 <4.0.0'
sdk: '>=2.18.5 <3.0.0'
dependencies:
flutter:
sdk: flutter
cupertino_icons: 1.0.8
cupertino_icons: 1.0.5
flutter_localizations:
sdk: flutter
flutter_translate: 4.1.0
provider: 6.1.2
stacked: 3.4.3
get_it: 7.7.0
logger: 2.4.0
shared_preferences: 2.3.2
http: 1.2.2
flutter_translate: 4.0.3
provider: 6.0.5
stacked: 3.0.1
get_it: 7.2.0
logger: 1.1.0
shared_preferences: 2.0.15
http: 0.13.5
validators: 3.0.0
flutter_linkify: 6.0.0
url_launcher: 6.3.1
flutter_linkify: 5.0.2
url_launcher: 6.1.7
expandable: 5.0.1
share_plus: 10.1.1
file_picker: 8.1.3
share_plus: 6.3.0
file_picker: 5.2.4
clipboard: 0.1.3
permission_handler: 11.3.1
package_info_plus: 8.1.0
json_annotation: 4.9.0
dynamic_color: 1.7.0
intl: 0.19.0
path: 1.9.0
flutter_sharing_intent: 1.1.1
device_info_plus: 11.1.0
receive_sharing_intent: 1.4.5
permission_handler: 10.2.0
package_info_plus: 3.0.2
json_annotation: 4.7.0
dynamic_color: 1.5.4
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: 2.4.13
built_value_generator: 8.9.2
json_serializable: 6.8.0
flutter_lints: 5.0.0
build_runner: 2.3.2
built_value_generator: 8.4.2
json_serializable: 6.5.4
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec

View file

@ -1,38 +0,0 @@
{
"$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"
}
]
}