diff --git a/.github/workflows/build_push_docker_latest.yml b/.github/workflows/build_push_docker_latest.yml
index 2fb4792366..b69d5916f2 100644
--- a/.github/workflows/build_push_docker_latest.yml
+++ b/.github/workflows/build_push_docker_latest.yml
@@ -17,17 +17,17 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -45,17 +45,17 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -72,17 +72,17 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -100,17 +100,17 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile
diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml
index 902a7a62df..36f2f1f568 100644
--- a/.github/workflows/build_push_docker_staging.yml
+++ b/.github/workflows/build_push_docker_staging.yml
@@ -2,8 +2,6 @@ name: Build and Push Docker Image - Staging
on:
workflow_dispatch:
- push:
- branches: [main]
pull_request:
branches: [main]
@@ -19,10 +17,10 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -30,7 +28,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Immich Mono Repo
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -38,6 +36,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-server:staging
+ altran1502/immich-server:${{ github.event.pull_request.number }}
build_and_push_machine_learning_staging:
runs-on: ubuntu-latest
@@ -48,10 +47,10 @@ jobs:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -59,7 +58,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -67,6 +66,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-machine-learning:staging
+ altran1502/immich-machine-learning:${{ github.event.pull_request.number }}
build_and_push_web_staging:
runs-on: ubuntu-latest
@@ -76,10 +76,10 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -87,7 +87,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Web
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -96,6 +96,7 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-web:staging
+ altran1502/immich-web:${{ github.event.pull_request.number }}
build_and_push_nginx_staging:
runs-on: ubuntu-latest
@@ -105,10 +106,10 @@ jobs:
with:
fetch-depth: 0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
if: ${{ github.repository == 'immich-app/immich' }}
uses: docker/login-action@v2
@@ -116,7 +117,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Proxy
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile
@@ -124,3 +125,4 @@ jobs:
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
tags: |
altran1502/immich-proxy:staging
+ altran1502/immich-proxy:${{ github.event.pull_request.number }}
diff --git a/.github/workflows/build_push_server_release.yml b/.github/workflows/build_push_server_release.yml
index 0db75d6b70..3f9a7a74fd 100644
--- a/.github/workflows/build_push_server_release.yml
+++ b/.github/workflows/build_push_server_release.yml
@@ -22,11 +22,11 @@ jobs:
fallback: latest
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -35,7 +35,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-server release
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./server
file: ./server/Dockerfile
@@ -58,17 +58,17 @@ jobs:
with:
fallback: latest
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Machine Learning
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./machine-learning
file: ./machine-learning/Dockerfile
@@ -94,11 +94,11 @@ jobs:
fallback: latest
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -107,7 +107,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-web release
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./web
file: ./web/Dockerfile
@@ -134,11 +134,11 @@ jobs:
fallback: latest
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2.0.0
+ uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v2.0.0
+ uses: docker/setup-buildx-action@v2.1.0
- name: Login to Docker Hub
uses: docker/login-action@v2
@@ -147,7 +147,7 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push immich-proxy release
- uses: docker/build-push-action@v3.1.1
+ uses: docker/build-push-action@v3.2.0
with:
context: ./nginx
file: ./nginx/Dockerfile
diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml
new file mode 100644
index 0000000000..91731a0256
--- /dev/null
+++ b/.github/workflows/openapi-generator.yml
@@ -0,0 +1,83 @@
+name: Generate OpenAPI SDK
+
+on:
+ workflow_dispatch:
+ push:
+ branches: [main]
+
+jobs:
+ generate-typescript-axios:
+ runs-on: ubuntu-latest
+ name: OpenAPI Generator
+ steps:
+ # Checkout your code
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ token: ${{ secrets.GH_TOKEN }}
+
+ # Use the action to generate a client package
+ # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace.
+ - name: Generate Typescript Axios Client
+ uses: openapi-generators/openapitools-generator-action@v1
+ with:
+ generator: typescript-axios
+ generator-tag: v6.2.0
+ openapi-file: server/immich-openapi-specs.json
+
+ # Do something with the generated client (likely publishing it somewhere)
+ - name: Push to typescript repo
+ run: |
+ git config --global init.defaultBranch main
+ git config --global pull.rebase false
+ git config --global user.email "alex.tran1502@gmail.com"
+ git config --global user.name "Alex Tran"
+ cd typescript-axios-client
+ git init
+ git add .
+ git commit -m "Update SDK"
+ git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-typescript-axios.git
+ git pull origin main --allow-unrelated-histories
+ git push origin main 2>&1 | grep -v 'To https'
+
+ - name: Generate Dart SDK
+ uses: openapi-generators/openapitools-generator-action@v1
+ with:
+ generator: dart
+ generator-tag: v6.2.0
+ openapi-file: server/immich-openapi-specs.json
+
+ - name: Push to Dart repo
+ run: |
+ git config --global init.defaultBranch main
+ git config --global pull.rebase false
+ git config --global user.email "alex.tran1502@gmail.com"
+ git config --global user.name "Alex Tran"
+ cd dart-client
+ git init
+ git add .
+ git commit -m "Update SDK"
+ git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-dart.git
+ git pull origin main --allow-unrelated-histories
+ git push origin main 2>&1 | grep -v 'To https'
+
+ - name: Generate Rust SDK
+ uses: openapi-generators/openapitools-generator-action@v1
+ with:
+ generator: rust
+ generator-tag: v6.2.0
+ openapi-file: server/immich-openapi-specs.json
+
+ - name: Push to Rust repo
+ run: |
+ git config --global init.defaultBranch main
+ git config --global pull.rebase false
+ git config --global user.email "alex.tran1502@gmail.com"
+ git config --global user.name "Alex Tran"
+ cd rust-client
+ git init
+ git add .
+ git commit -m "Update SDK"
+ git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-rust.git
+ git pull origin main --allow-unrelated-histories
+ git push origin main 2>&1 | grep -v 'To https'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 60a379d8d5..db79ec0677 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,7 +15,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
- - name: Run Immich Server 2E2 Test
+ - name: Run Immich Server E2E Test
run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test
server-unit-tests:
diff --git a/README.md b/README.md
index ac776b25c2..dd6015870a 100644
--- a/README.md
+++ b/README.md
@@ -46,13 +46,14 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
- [Installation](#installation)
- [Update](#update)
- [Mobile App](#mobile-app)
+- [App Beta Invitation links](#App-Beta-release-channel)
- [Development](#development)
- [Support](#support)
- [Known Issues](#known-issues)
# Features
-> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes.
+> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development. There will be continuous functions, features and api changes.
| Features | Mobile | Web |
| - | - | - |
@@ -117,11 +118,11 @@ There are several services that compose Immich:
NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX).
-## Testing One-step installation (not recommended for production)
+## Testing one-step installation (not recommended for production)
-> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.*
+> ⚠️ *This installation method is for evaluating Immich before further customization to meet the users' needs.*
-*Applicable system: Ubuntu, Debian, MacOS*
+*Applicable operating systems: Ubuntu, Debian, MacOS*
- In the shell, from the directory of your choice, run the following command:
@@ -203,9 +204,13 @@ docker-compose pull && docker-compose up -d
| - | - | - |
|
|
|
|
-> *The Play/App Store version might be lagging behind the latest release due to the review process.*
+> *The Play/App Store version might be lagging behind the latest release due to their review process.*
+# App Beta release channel
+You can opt-in to join app beta release channel by following the links below:
+* Android: Invitation link from [web](https://play.google.com/store/apps/details?id=app.alextran.immich) or from [mobile](https://play.google.com/store/apps/details?id=app.alextran.immich)
+* iOS: [TestFlight invitation link](https://testflight.apple.com/join/1vYsAa8P)
# Development
diff --git a/docker/.env.example b/docker/.env.example
index 33b4925514..7b398969da 100644
--- a/docker/.env.example
+++ b/docker/.env.example
@@ -38,7 +38,10 @@ LOG_LEVEL=simple
# JWT SECRET
###################################################################################
-JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
+# This JWT_SECRET is used to sign the authentication keys for user login
+# You should set it to a long randomly generated value
+# You can use this command to generate one: openssl rand -base64 128
+JWT_SECRET=
###################################################################################
# Reverse Geocoding
diff --git a/install.sh b/install.sh
index 1701d33cc2..dbe0d8db6a 100755
--- a/install.sh
+++ b/install.sh
@@ -18,33 +18,37 @@ get_release_version() {
create_immich_directory() {
echo "Creating Immich directory..."
mkdir -p ./immich-app/immich-data
+ cd ./immich-app
}
download_docker_compose_file() {
echo "Downloading docker-compose.yml..."
- curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1
+ curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1
}
download_dot_env_file() {
echo "Downloading .env file..."
- curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1
+ curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./.env >/dev/null 2>&1
+}
+
+replace_env_value() {
+ if [[ "$OSTYPE" == "darwin"* ]]; then
+ sed -i '' "s|$1=.*|$1=$2|" ./.env
+ else
+ sed -i "s|$1=.*|$1=$2|" ./.env
+ fi
}
populate_upload_location() {
echo "Populating default UPLOAD_LOCATION value..."
+ upload_location=$(pwd)/immich-data
+ replace_env_value "UPLOAD_LOCATION" $upload_location
+}
- cd ./immich-app/immich-data
-
- upload_location=$(pwd)
-
- # Replace value of UPLOAD_LOCATION in .env with upload_location path
- if [[ "$OSTYPE" == "darwin"* ]]; then
- sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
- else
- sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env
- fi
-
- cd ..
+generate_jwt_secret() {
+ echo "Generating JWT_SECRET value..."
+ jwt_secret=$(openssl rand -base64 128)
+ replace_env_value "JWT_SECRET" $jwt_secret
}
start_docker_compose() {
@@ -88,4 +92,5 @@ create_immich_directory
download_docker_compose_file
download_dot_env_file
populate_upload_location
+generate_jwt_secret
start_docker_compose
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index af00aac413..36a967da6c 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -1,5 +1,5 @@
-
-
+
+
-
-
+
+
+
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt
deleted file mode 100644
index bbdaa27f5f..0000000000
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package app.alextran.immich
-
-import android.app.Service
-import android.content.Intent
-import android.os.IBinder
-
-/**
- * Catches the event when either the system or the user kills the app
- * (does not apply on force close!)
- */
-class AppClearedService() : Service() {
-
- override fun onBind(intent: Intent): IBinder? {
- return null
- }
-
- override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
- return START_NOT_STICKY;
- }
-
- override fun onTaskRemoved(rootIntent: Intent) {
- ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext)
- stopSelf();
- }
-}
\ No newline at end of file
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
index bebaa579be..3cb231eaf6 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
@@ -10,7 +10,7 @@ import io.flutter.plugin.common.MethodChannel
* Android plugin for Dart `BackgroundService`
*
* Receives messages/method calls from the foreground Dart side to manage
- * the background service, e.g. start (enqueue), stop (cancel)
+ * the background service, e.g. start (enqueue), stop (cancel)
*/
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
@@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val ctx = context!!
- when(call.method) {
+ when (call.method) {
"enable" -> {
val args = call.arguments>()!!
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .edit()
- .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
- .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
- .apply()
+ .edit()
+ .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
+ .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
+ .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
+ .apply()
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
result.success(true)
}
@@ -54,7 +55,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val requireUnmeteredNetwork = args.get(0) as Boolean
val requireCharging = args.get(1) as Boolean
ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
- result.success(true)
+ result.success(true)
}
"disable" -> {
ContentObserverWorker.disable(ctx)
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
index 24bbd1785d..116422634c 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
@@ -1,5 +1,6 @@
package app.alextran.immich
+import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
@@ -47,6 +48,8 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
private var timeBackupStarted: Long = 0L
+ private var notificationBuilder: NotificationCompat.Builder? = null
+ private var notificationDetailBuilder: NotificationCompat.Builder? = null
override fun startWork(): ListenableFuture {
@@ -61,16 +64,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
// Create a Notification channel if necessary
createChannel()
}
- val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
- setForegroundAsync(createForegroundInfo(title))
- } else {
- showBackgroundInfo(title)
+ val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
+ showInfo(getInfoBuilder(title, indeterminate=true).build())
}
engine = FlutterEngine(ctx)
@@ -154,18 +155,21 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
"updateNotification" -> {
val args = call.arguments>()!!
- val title = args.get(0) as String
- val content = args.get(1) as String
- if (isIgnoringBatteryOptimizations) {
- setForegroundAsync(createForegroundInfo(title, content))
- } else {
- showBackgroundInfo(title, content)
+ val title = args.get(0) as String?
+ val content = args.get(1) as String?
+ val progress = args.get(2) as Int
+ val max = args.get(3) as Int
+ val indeterminate = args.get(4) as Boolean
+ val isDetail = args.get(5) as Boolean
+ val onlyIfFG = args.get(6) as Boolean
+ if (!onlyIfFG || isIgnoringBatteryOptimizations) {
+ showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail)
}
}
"showError" -> {
val args = call.arguments>()!!
val title = args.get(0) as String
- val content = args.get(1) as String
+ val content = args.get(1) as String?
val individualTag = args.get(2) as String?
showError(title, content, individualTag)
}
@@ -182,13 +186,12 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
}
}
- private fun showError(title: String, content: String, individualTag: String?) {
+ private fun showError(title: String, content: String?, individualTag: String?) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.mipmap.ic_launcher)
- .setOnlyAlertOnce(true)
.build()
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
@@ -197,38 +200,54 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
notificationManager.cancel(NOTIFICATION_ERROR_ID)
}
- private fun showBackgroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null) {
- val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
- .setContentTitle(title)
- .setTicker(title)
- .setContentText(content)
- .setSmallIcon(R.mipmap.ic_launcher)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- .build()
- notificationManager.notify(NOTIFICATION_ID, notification)
- }
-
private fun clearBackgroundNotification() {
notificationManager.cancel(NOTIFICATION_ID)
+ notificationManager.cancel(NOTIFICATION_DETAIL_ID)
}
- private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
- val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
- .setContentTitle(title)
- .setTicker(title)
- .setContentText(content)
- .setSmallIcon(R.mipmap.ic_launcher)
- .setOngoing(true)
- .build()
- return ForegroundInfo(NOTIFICATION_ID, notification)
- }
+ private fun showInfo(notification: Notification, isDetail: Boolean = false) {
+ val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
+ if (isIgnoringBatteryOptimizations) {
+ setForegroundAsync(ForegroundInfo(id, notification))
+ } else {
+ notificationManager.notify(id, notification)
+ }
+ }
+
+ private fun getInfoBuilder(
+ title: String? = null,
+ content: String? = null,
+ isDetail: Boolean = false,
+ progress: Int = 0,
+ max: Int = 0,
+ indeterminate: Boolean = false,
+ ): NotificationCompat.Builder {
+ var builder = if(isDetail) notificationDetailBuilder else notificationBuilder
+ if (builder == null) {
+ builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setOnlyAlertOnce(true)
+ .setOngoing(true)
+ if (isDetail) {
+ notificationDetailBuilder = builder
+ } else {
+ notificationBuilder = builder
+ }
+ }
+ if (title != null) {
+ builder.setTicker(title).setContentTitle(title)
+ }
+ if (content != null) {
+ builder.setContentText(content)
+ }
+ return builder.setProgress(max, progress, indeterminate)
+ }
@RequiresApi(Build.VERSION_CODES.O)
private fun createChannel() {
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(foreground)
- val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT)
+ val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
notificationManager.createNotificationChannel(error)
}
@@ -244,6 +263,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
+ private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L
/**
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
index ecbec640fa..a58ea14518 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
@@ -46,9 +46,6 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
* @param context Android Context
*/
fun enable(context: Context, immediate: Boolean = false) {
- // migration to remove any old active background task
- WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener")
-
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
Log.d(TAG, "enabled ContentObserverWorker")
if (immediate) {
@@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
}
- private fun startBackupWorker(context: Context, delayMilliseconds: Long) {
+ fun startBackupWorker(context: Context, delayMilliseconds: Long) {
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false))
+ return
val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt
new file mode 100644
index 0000000000..86b82d2be9
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt
@@ -0,0 +1,19 @@
+package app.alextran.immich
+
+import android.app.Application
+import androidx.work.Configuration
+import androidx.work.WorkManager
+
+class ImmichApp : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ val config = Configuration.Builder().build()
+ WorkManager.initialize(this, config)
+ // always start BackupWorker after WorkManager init; this fixes the following bug:
+ // After the process is killed (by user or system), the first trigger (taking a new picture) is lost.
+ // Thus, the BackupWorker is not started. If the system kills the process after each initialization
+ // (because of low memory etc.), the backup is never performed.
+ // As a workaround, we also run a backup check when initializing the application
+ ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0)
+ }
+}
\ No newline at end of file
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt
index 2e6372231d..5df36cb18f 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt
@@ -5,21 +5,11 @@ import io.flutter.embedding.engine.FlutterEngine
import android.os.Bundle
import android.content.Intent
-class MainActivity: FlutterActivity() {
+class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
- flutterEngine.getPlugins().add(BackgroundServicePlugin())
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- try {
- startService(Intent(getBaseContext(), AppClearedService::class.java));
- } catch (e: Exception) {
- // startService must not be called when app is in background (crashes app)
- // there is nothing we can do
- }
+ flutterEngine.plugins.add(BackgroundServicePlugin())
}
}
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index c0fc2fe307..bfa80f3aba 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -16,12 +16,17 @@
default_platform(:android)
platform :android do
- desc "Build Android"
- lane :build do
+ desc "Build Android and Release Testing"
+ lane :beta do
gradle(
task: 'bundle',
build_type: 'Release',
+ properties: {
+ "android.injected.version.code" => 47,
+ "android.injected.version.name" => "1.30.2",
+ }
)
+ upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta')
end
desc "Build and Release Android"
@@ -30,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 46,
- "android.injected.version.name" => "1.30.0",
+ "android.injected.version.code" => 49,
+ "android.injected.version.name" => "1.31.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/android/fastlane/README.md b/mobile/android/fastlane/README.md
index fb4b573aac..11cb12000a 100644
--- a/mobile/android/fastlane/README.md
+++ b/mobile/android/fastlane/README.md
@@ -15,13 +15,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
## Android
-### android build
+### android beta
```sh
-[bundle exec] fastlane android build
+[bundle exec] fastlane android beta
```
-Build Android
+Build Android and Release Testing
### android release
diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt
new file mode 100644
index 0000000000..a97d899715
--- /dev/null
+++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt
@@ -0,0 +1 @@
+* Improve scroll thumb date info
\ No newline at end of file
diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt
new file mode 100644
index 0000000000..d6e2aac53c
--- /dev/null
+++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt
@@ -0,0 +1 @@
+* Fixed parsing date error prevent timeline to be loaded.
\ No newline at end of file
diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt
new file mode 100644
index 0000000000..90360a3c0c
--- /dev/null
+++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt
@@ -0,0 +1,2 @@
+* Fixed run background service after being killed
+* Added background backup progress notifications
\ No newline at end of file
diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml
index 7ec06ddace..35596dd5e1 100644
--- a/mobile/android/fastlane/report.xml
+++ b/mobile/android/fastlane/report.xml
@@ -5,17 +5,17 @@
-
+
-
+
-
+
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index c81ebdfe63..d2cd917519 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -134,6 +134,10 @@
"setting_notifications_notify_never": "never",
"setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_title": "Notifications",
+ "setting_notifications_total_progress_title": "Show background backup total progress",
+ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
+ "setting_notifications_single_progress_title": "Show background backup detail progress",
+ "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset",
"setting_pages_app_bar_settings": "Settings",
"share_add": "Add",
"share_add_photos": "Add photos",
diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj
index f5f19fdccc..d3518d6393 100644
--- a/mobile/ios/Runner.xcodeproj/project.pbxproj
+++ b/mobile/ios/Runner.xcodeproj/project.pbxproj
@@ -360,7 +360,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 60;
+ CURRENT_PROJECT_VERSION = 62;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -495,7 +495,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 60;
+ CURRENT_PROJECT_VERSION = 62;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -522,7 +522,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 60;
+ CURRENT_PROJECT_VERSION = 62;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 80140355d9..639862b8b2 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -17,11 +17,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.29.6
+ 1.30.1
CFBundleSignature
????
CFBundleVersion
- 60
+ 62
LSRequiresIPhoneOS
MGLMapboxMetricsEnabledSettingShownInApp
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 41089b5b15..1a584c1ad8 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
- version_number: "1.30.0"
+ version_number: "1.31.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml
index 632cdf7c9b..61fe97cbff 100644
--- a/mobile/ios/fastlane/report.xml
+++ b/mobile/ios/fastlane/report.xml
@@ -5,32 +5,32 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart
index 1af6dc9816..f5a0086b5b 100644
--- a/mobile/lib/modules/backup/background_service/background.service.dart
+++ b/mobile/lib/modules/backup/background_service/background.service.dart
@@ -27,11 +27,11 @@ final backgroundServiceProvider = Provider(
/// Background backup service
class BackgroundService {
static const String _portNameLock = "immichLock";
- BackgroundService();
static const MethodChannel _foregroundChannel =
MethodChannel('immich/foregroundChannel');
static const MethodChannel _backgroundChannel =
MethodChannel('immich/backgroundChannel');
+ static final NumberFormat numberFormat = NumberFormat("###0.##");
bool _isBackgroundInitialized = false;
CancellationToken? _cancellationToken;
bool _canceledBySystem = false;
@@ -40,6 +40,10 @@ class BackgroundService {
SendPort? _waitingIsolate;
ReceivePort? _rp;
bool _errorGracePeriodExceeded = true;
+ int _uploadedAssetsCount = 0;
+ int _assetsToUploadCount = 0;
+ int _lastDetailProgressUpdate = 0;
+ String _lastPrintedProgress = "";
bool get isBackgroundInitialized {
return _isBackgroundInitialized;
@@ -125,22 +129,29 @@ class BackgroundService {
}
/// Updates the notification shown by the background service
- Future _updateNotification({
- required String title,
+ Future _updateNotification({
+ String? title,
String? content,
+ int progress = 0,
+ int max = 0,
+ bool indeterminate = false,
+ bool isDetail = false,
+ bool onlyIfFG = false,
}) async {
if (!Platform.isAndroid) {
return true;
}
try {
if (_isBackgroundInitialized) {
- return await _backgroundChannel
- .invokeMethod('updateNotification', [title, content]);
+ return _backgroundChannel.invokeMethod(
+ 'updateNotification',
+ [title, content, progress, max, indeterminate, isDetail, onlyIfFG],
+ );
}
} catch (error) {
debugPrint("[_updateNotification] failed to communicate with plugin");
}
- return Future.value(false);
+ return false;
}
/// Shows a new priority notification
@@ -274,6 +285,7 @@ class BackgroundService {
case "onAssetsChanged":
final Future translationsLoaded = loadTranslations();
try {
+ _clearErrorNotifications();
final bool hasAccess = await acquireLock();
if (!hasAccess) {
debugPrint("[_callHandler] could not acquire lock, exiting");
@@ -313,19 +325,23 @@ class BackgroundService {
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
BackupService backupService = BackupService(apiService);
+ AppSettingsService settingsService = AppSettingsService();
final Box box =
await Hive.openBox(hiveBackupInfoBox);
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
if (backupAlbumInfo == null) {
- _clearErrorNotifications();
return true;
}
await PhotoManager.setIgnorePermissionCheck(true);
do {
- final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
+ final bool backupOk = await _runBackup(
+ backupService,
+ settingsService,
+ backupAlbumInfo,
+ );
if (backupOk) {
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
await box.put(
@@ -346,9 +362,14 @@ class BackgroundService {
Future _runBackup(
BackupService backupService,
+ AppSettingsService settingsService,
HiveBackupAlbums backupAlbumInfo,
) async {
- _errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
+ _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
+ final bool notifyTotalProgress = settingsService
+ .getSetting(AppSettingsEnum.backgroundBackupTotalProgress);
+ final bool notifySingleProgress = settingsService
+ .getSetting(AppSettingsEnum.backgroundBackupSingleProgress);
if (_canceledBySystem) {
return false;
@@ -372,22 +393,29 @@ class BackgroundService {
}
if (toUpload.isEmpty) {
- _clearErrorNotifications();
return true;
}
+ _assetsToUploadCount = toUpload.length;
+ _uploadedAssetsCount = 0;
+ _updateNotification(
+ title: "backup_background_service_in_progress_notification".tr(),
+ content: notifyTotalProgress ? _formatAssetBackupProgress() : null,
+ progress: 0,
+ max: notifyTotalProgress ? _assetsToUploadCount : 0,
+ indeterminate: !notifyTotalProgress,
+ onlyIfFG: !notifyTotalProgress,
+ );
_cancellationToken = CancellationToken();
final bool ok = await backupService.backupAsset(
toUpload,
_cancellationToken!,
- _onAssetUploaded,
- _onProgress,
- _onSetCurrentBackupAsset,
+ notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {},
+ notifySingleProgress ? _onProgress : (sent, total) {},
+ notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
_onBackupError,
);
- if (ok) {
- _clearErrorNotifications();
- } else {
+ if (!ok && !_cancellationToken!.isCancelled) {
_showErrorNotification(
title: "backup_background_service_error_title".tr(),
content: "backup_background_service_backup_failed_message".tr(),
@@ -396,16 +424,43 @@ class BackgroundService {
return ok;
}
- void _onAssetUploaded(String deviceAssetId, String deviceId) {
- debugPrint("Uploaded $deviceAssetId from $deviceId");
+ String _formatAssetBackupProgress() {
+ final int percent = (_uploadedAssetsCount * 100) ~/ _assetsToUploadCount;
+ return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
}
- void _onProgress(int sent, int total) {}
+ void _onAssetUploaded(String deviceAssetId, String deviceId) {
+ debugPrint("Uploaded $deviceAssetId from $deviceId");
+ _uploadedAssetsCount++;
+ _updateNotification(
+ progress: _uploadedAssetsCount,
+ max: _assetsToUploadCount,
+ content: _formatAssetBackupProgress(),
+ );
+ }
+
+ void _onProgress(int sent, int total) {
+ final int now = Timeline.now;
+ // limit updates to 10 per second (or Android drops important notifications)
+ if (now > _lastDetailProgressUpdate + 100000) {
+ final String msg = _humanReadableBytesProgress(sent, total);
+ // only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
+ if (msg != _lastPrintedProgress) {
+ _lastDetailProgressUpdate = now;
+ _lastPrintedProgress = msg;
+ _updateNotification(
+ progress: sent,
+ max: total,
+ isDetail: true,
+ content: msg,
+ );
+ }
+ }
+ }
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
_showErrorNotification(
- title: "Upload failed",
- content: "backup_background_service_upload_failure_notification"
+ title: "backup_background_service_upload_failure_notification"
.tr(args: [errorAssetInfo.fileName]),
individualTag: errorAssetInfo.id,
);
@@ -413,14 +468,17 @@ class BackgroundService {
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
_updateNotification(
- title: "backup_background_service_in_progress_notification".tr(),
- content: "backup_background_service_current_upload_notification"
+ title: "backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]),
+ content: "",
+ isDetail: true,
+ progress: 0,
+ max: 0,
);
}
- bool _isErrorGracePeriodExceeded() {
- final int value = AppSettingsService()
+ bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) {
+ final int value = appSettingsService
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
if (value == 0) {
return true;
@@ -445,6 +503,26 @@ class BackgroundService {
assert(false, "Invalid value");
return true;
}
+
+ /// prints percentage and absolute progress in useful (kilo/mega/giga)bytes
+ static String _humanReadableBytesProgress(int bytes, int bytesTotal) {
+ String unit = "KB"; // Kilobyte
+ if (bytesTotal >= 0x40000000) {
+ unit = "GB"; // Gigabyte
+ bytes >>= 20;
+ bytesTotal >>= 20;
+ } else if (bytesTotal >= 0x100000) {
+ unit = "MB"; // Megabyte
+ bytes >>= 10;
+ bytesTotal >>= 10;
+ } else if (bytesTotal < 0x400) {
+ return "$bytes / $bytesTotal B";
+ }
+ final int percent = (bytes * 100) ~/ bytesTotal;
+ final String done = numberFormat.format(bytes / 1024.0);
+ final String total = numberFormat.format(bytesTotal / 1024.0);
+ return "$percent% ($done/$total$unit)";
+ }
}
/// entry point called by Kotlin/Java code; needs to be a top-level function
diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart
index 707f98fb7d..f97fd537e0 100644
--- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart
+++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart
@@ -1,5 +1,3 @@
-import 'dart:math';
-
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
index 74124f76ef..775f1b0c4b 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
@@ -1,11 +1,8 @@
import 'dart:collection';
-import 'dart:math';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/src/widgets/framework.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:openapi/api.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
@@ -15,7 +12,9 @@ import 'disable_multi_select_button.dart';
import 'draggable_scrollbar_custom.dart';
typedef ImmichAssetGridSelectionListener = void Function(
- bool, Set);
+ bool,
+ Set,
+);
class ImmichAssetGridState extends State {
final ItemScrollController _itemScrollController = ItemScrollController();
@@ -23,7 +22,7 @@ class ImmichAssetGridState extends State {
ItemPositionsListener.create();
bool _scrolling = false;
- Set _selectedAssets = HashSet();
+ final Set _selectedAssets = HashSet();
List get _assets {
return widget.renderList
@@ -86,7 +85,9 @@ class ImmichAssetGridState extends State {
}
Widget _buildThumbnailOrPlaceholder(
- AssetResponseDto asset, bool placeholder) {
+ AssetResponseDto asset,
+ bool placeholder,
+ ) {
if (placeholder) {
return const DecoratedBox(
decoration: BoxDecoration(color: Colors.grey),
@@ -104,7 +105,10 @@ class ImmichAssetGridState extends State {
}
Widget _buildAssetRow(
- BuildContext context, RenderAssetGridRow row, bool scrolling) {
+ BuildContext context,
+ RenderAssetGridRow row,
+ bool scrolling,
+ ) {
double size = _getItemSize(context);
return Row(
@@ -117,7 +121,9 @@ class ImmichAssetGridState extends State {
width: size,
height: size,
margin: EdgeInsets.only(
- top: widget.margin, right: last ? 0.0 : widget.margin),
+ top: widget.margin,
+ right: last ? 0.0 : widget.margin,
+ ),
child: _buildThumbnailOrPlaceholder(asset, scrolling),
);
}).toList(),
@@ -125,7 +131,10 @@ class ImmichAssetGridState extends State {
}
Widget _buildTitle(
- BuildContext context, String title, List assets) {
+ BuildContext context,
+ String title,
+ List assets,
+ ) {
return DailyTitleText(
isoDate: title,
multiselectEnabled: widget.selectionActive,
@@ -186,7 +195,7 @@ class ImmichAssetGridState extends State {
}
Widget _buildAssetGrid() {
- final useDragScrolling = _assets.length > 100;
+ final useDragScrolling = _assets.length >= 20;
void dragScrolling(bool active) {
setState(() {
@@ -218,7 +227,6 @@ class ImmichAssetGridState extends State {
);
}
-
@override
void didUpdateWidget(ImmichAssetGrid oldWidget) {
super.didUpdateWidget(oldWidget);
@@ -248,14 +256,14 @@ class ImmichAssetGrid extends StatefulWidget {
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
- ImmichAssetGrid({
+ const ImmichAssetGrid({
super.key,
required this.renderList,
required this.assetsPerRow,
required this.showStorageIndicator,
this.listener,
this.margin = 5.0,
- this.selectionActive = false
+ this.selectionActive = false,
});
@override
diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart
index 469e6a0d43..292c40c210 100644
--- a/mobile/lib/modules/settings/services/app_settings.service.dart
+++ b/mobile/lib/modules/settings/services/app_settings.service.dart
@@ -6,7 +6,11 @@ enum AppSettingsEnum {
themeMode("themeMode", "system"), // "light","dark","system"
tilesPerRow("tilesPerRow", 4),
uploadErrorNotificationGracePeriod(
- "uploadErrorNotificationGracePeriod", 2),
+ "uploadErrorNotificationGracePeriod",
+ 2,
+ ),
+ backgroundBackupTotalProgress("backgroundBackupTotalProgress", true),
+ backgroundBackupSingleProgress("backgroundBackupSingleProgress", false),
storageIndicator("storageIndicator", true),
thumbnailCacheSize("thumbnailCacheSize", 10000),
imageCacheSize("imageCacheSize", 350),
diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
index 1643a830b5..be988e01cb 100644
--- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
+++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart
@@ -15,12 +15,20 @@ class NotificationSetting extends HookConsumerWidget {
final appSettingService = ref.watch(appSettingsServiceProvider);
final sliderValue = useState(0.0);
+ final totalProgressValue =
+ useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
+ final singleProgressValue =
+ useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
useEffect(
() {
sliderValue.value = appSettingService
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod)
.toDouble();
+ totalProgressValue.value = appSettingService
+ .getSetting(AppSettingsEnum.backgroundBackupTotalProgress);
+ singleProgressValue.value = appSettingService
+ .getSetting(AppSettingsEnum.backgroundBackupSingleProgress);
return null;
},
[],
@@ -42,6 +50,22 @@ class NotificationSetting extends HookConsumerWidget {
),
).tr(),
children: [
+ _buildSwitchListTile(
+ context,
+ appSettingService,
+ totalProgressValue,
+ AppSettingsEnum.backgroundBackupTotalProgress,
+ title: 'setting_notifications_total_progress_title'.tr(),
+ subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
+ ),
+ _buildSwitchListTile(
+ context,
+ appSettingService,
+ singleProgressValue,
+ AppSettingsEnum.backgroundBackupSingleProgress,
+ title: 'setting_notifications_single_progress_title'.tr(),
+ subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
+ ),
ListTile(
isThreeLine: false,
dense: true,
@@ -53,7 +77,9 @@ class NotificationSetting extends HookConsumerWidget {
value: sliderValue.value,
onChanged: (double v) => sliderValue.value = v,
onChangeEnd: (double v) => appSettingService.setSetting(
- AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()),
+ AppSettingsEnum.uploadErrorNotificationGracePeriod,
+ v.toInt(),
+ ),
max: 5.0,
divisions: 5,
label: formattedValue,
@@ -65,6 +91,28 @@ class NotificationSetting extends HookConsumerWidget {
}
}
+SwitchListTile _buildSwitchListTile(
+ BuildContext context,
+ AppSettingsService appSettingService,
+ ValueNotifier valueNotifier,
+ AppSettingsEnum settingsEnum, {
+ required String title,
+ String? subtitle,
+}) {
+ return SwitchListTile(
+ key: Key(settingsEnum.name),
+ value: valueNotifier.value,
+ onChanged: (value) {
+ valueNotifier.value = value;
+ appSettingService.setSetting(settingsEnum, value);
+ },
+ activeColor: Theme.of(context).primaryColor,
+ dense: true,
+ title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
+ subtitle: subtitle != null ? Text(subtitle) : null,
+ );
+}
+
String _formatSliderValue(double v) {
if (v == 0.0) {
return 'setting_notifications_notify_immediately'.tr();
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index a5d73e1474..3a8edc0030 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -8,6 +8,7 @@ doc/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md
+doc/AllJobStatusResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeBucket.md
doc/AssetCountByTimeBucketResponseDto.md
@@ -33,6 +34,12 @@ doc/DeviceTypeEnum.md
doc/ExifResponseDto.md
doc/GetAssetByTimeBucketDto.md
doc/GetAssetCountByTimeBucketDto.md
+doc/JobApi.md
+doc/JobCommand.md
+doc/JobCommandDto.md
+doc/JobCounts.md
+doc/JobId.md
+doc/JobStatusResponseDto.md
doc/LoginCredentialDto.md
doc/LoginResponseDto.md
doc/LogoutResponseDto.md
@@ -59,6 +66,7 @@ lib/api/album_api.dart
lib/api/asset_api.dart
lib/api/authentication_api.dart
lib/api/device_info_api.dart
+lib/api/job_api.dart
lib/api/server_info_api.dart
lib/api/user_api.dart
lib/api_client.dart
@@ -74,6 +82,7 @@ lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart
+lib/model/all_job_status_response_dto.dart
lib/model/asset_count_by_time_bucket.dart
lib/model/asset_count_by_time_bucket_response_dto.dart
lib/model/asset_count_by_user_id_response_dto.dart
@@ -96,6 +105,11 @@ lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_by_time_bucket_dto.dart
lib/model/get_asset_count_by_time_bucket_dto.dart
+lib/model/job_command.dart
+lib/model/job_command_dto.dart
+lib/model/job_counts.dart
+lib/model/job_id.dart
+lib/model/job_status_response_dto.dart
lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 0d037e60b3..3154654d42 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -97,6 +97,9 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info |
*DeviceInfoApi* | [**updateDeviceInfo**](doc//DeviceInfoApi.md#updatedeviceinfo) | **PATCH** /device-info |
+*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
+*JobApi* | [**getJobStatus**](doc//JobApi.md#getjobstatus) | **GET** /jobs/{jobId} |
+*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
@@ -117,6 +120,7 @@ Class | Method | HTTP request | Description
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md)
+ - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
@@ -139,6 +143,11 @@ Class | Method | HTTP request | Description
- [ExifResponseDto](doc//ExifResponseDto.md)
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
+ - [JobCommand](doc//JobCommand.md)
+ - [JobCommandDto](doc//JobCommandDto.md)
+ - [JobCounts](doc//JobCounts.md)
+ - [JobId](doc//JobId.md)
+ - [JobStatusResponseDto](doc//JobStatusResponseDto.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
diff --git a/mobile/openapi/doc/AllJobStatusResponseDto.md b/mobile/openapi/doc/AllJobStatusResponseDto.md
new file mode 100644
index 0000000000..3fa53791df
--- /dev/null
+++ b/mobile/openapi/doc/AllJobStatusResponseDto.md
@@ -0,0 +1,22 @@
+# openapi.model.AllJobStatusResponseDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**thumbnailGenerationQueueCount** | [**JobCounts**](JobCounts.md) | |
+**metadataExtractionQueueCount** | [**JobCounts**](JobCounts.md) | |
+**videoConversionQueueCount** | [**JobCounts**](JobCounts.md) | |
+**machineLearningQueueCount** | [**JobCounts**](JobCounts.md) | |
+**isThumbnailGenerationActive** | **bool** | |
+**isMetadataExtractionActive** | **bool** | |
+**isVideoConversionActive** | **bool** | |
+**isMachineLearningActive** | **bool** | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/CreateJobDto.md b/mobile/openapi/doc/CreateJobDto.md
new file mode 100644
index 0000000000..64cdbf0184
--- /dev/null
+++ b/mobile/openapi/doc/CreateJobDto.md
@@ -0,0 +1,15 @@
+# openapi.model.CreateJobDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**jobType** | [**JobType**](JobType.md) | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/ExifResponseDto.md b/mobile/openapi/doc/ExifResponseDto.md
index 0e96bdcbe9..af4bb349ec 100644
--- a/mobile/openapi/doc/ExifResponseDto.md
+++ b/mobile/openapi/doc/ExifResponseDto.md
@@ -8,13 +8,13 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
-**id** | **String** | | [optional]
+**id** | **int** | | [optional]
+**fileSizeInByte** | **int** | | [optional]
**make** | **String** | | [optional]
**model** | **String** | | [optional]
**imageName** | **String** | | [optional]
**exifImageWidth** | **num** | | [optional]
**exifImageHeight** | **num** | | [optional]
-**fileSizeInByte** | **num** | | [optional]
**orientation** | **String** | | [optional]
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]
diff --git a/mobile/openapi/doc/JobApi.md b/mobile/openapi/doc/JobApi.md
new file mode 100644
index 0000000000..124e3d2149
--- /dev/null
+++ b/mobile/openapi/doc/JobApi.md
@@ -0,0 +1,155 @@
+# openapi.api.JobApi
+
+## Load the API package
+```dart
+import 'package:openapi/api.dart';
+```
+
+All URIs are relative to */api*
+
+Method | HTTP request | Description
+------------- | ------------- | -------------
+[**getAllJobsStatus**](JobApi.md#getalljobsstatus) | **GET** /jobs |
+[**getJobStatus**](JobApi.md#getjobstatus) | **GET** /jobs/{jobId} |
+[**sendJobCommand**](JobApi.md#sendjobcommand) | **PUT** /jobs/{jobId} |
+
+
+# **getAllJobsStatus**
+> AllJobStatusResponseDto getAllJobsStatus()
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = JobApi();
+
+try {
+ final result = api_instance.getAllJobsStatus();
+ print(result);
+} catch (e) {
+ print('Exception when calling JobApi->getAllJobsStatus: $e\n');
+}
+```
+
+### Parameters
+This endpoint does not need any parameter.
+
+### Return type
+
+[**AllJobStatusResponseDto**](AllJobStatusResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **getJobStatus**
+> JobStatusResponseDto getJobStatus(jobId)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = JobApi();
+final jobId = ; // JobId |
+
+try {
+ final result = api_instance.getJobStatus(jobId);
+ print(result);
+} catch (e) {
+ print('Exception when calling JobApi->getJobStatus: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **jobId** | [**JobId**](.md)| |
+
+### Return type
+
+[**JobStatusResponseDto**](JobStatusResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
+# **sendJobCommand**
+> num sendJobCommand(jobId, jobCommandDto)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = JobApi();
+final jobId = ; // JobId |
+final jobCommandDto = JobCommandDto(); // JobCommandDto |
+
+try {
+ final result = api_instance.sendJobCommand(jobId, jobCommandDto);
+ print(result);
+} catch (e) {
+ print('Exception when calling JobApi->sendJobCommand: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **jobId** | [**JobId**](.md)| |
+ **jobCommandDto** | [**JobCommandDto**](JobCommandDto.md)| |
+
+### Return type
+
+**num**
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
diff --git a/mobile/openapi/doc/JobCommand.md b/mobile/openapi/doc/JobCommand.md
new file mode 100644
index 0000000000..620e0439a5
--- /dev/null
+++ b/mobile/openapi/doc/JobCommand.md
@@ -0,0 +1,14 @@
+# openapi.model.JobCommand
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/JobCommandDto.md b/mobile/openapi/doc/JobCommandDto.md
new file mode 100644
index 0000000000..4e87fde8e8
--- /dev/null
+++ b/mobile/openapi/doc/JobCommandDto.md
@@ -0,0 +1,15 @@
+# openapi.model.JobCommandDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**command** | [**JobCommand**](JobCommand.md) | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/JobCounts.md b/mobile/openapi/doc/JobCounts.md
new file mode 100644
index 0000000000..353b834382
--- /dev/null
+++ b/mobile/openapi/doc/JobCounts.md
@@ -0,0 +1,19 @@
+# openapi.model.JobCounts
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**active** | **int** | |
+**completed** | **int** | |
+**failed** | **int** | |
+**delayed** | **int** | |
+**waiting** | **int** | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/JobId.md b/mobile/openapi/doc/JobId.md
new file mode 100644
index 0000000000..d2f68234d0
--- /dev/null
+++ b/mobile/openapi/doc/JobId.md
@@ -0,0 +1,14 @@
+# openapi.model.JobId
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/JobStatusResponseDto.md b/mobile/openapi/doc/JobStatusResponseDto.md
new file mode 100644
index 0000000000..13325a5152
--- /dev/null
+++ b/mobile/openapi/doc/JobStatusResponseDto.md
@@ -0,0 +1,16 @@
+# openapi.model.JobStatusResponseDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**isActive** | **bool** | |
+**queueCount** | [**Object**](.md) | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/JobType.md b/mobile/openapi/doc/JobType.md
new file mode 100644
index 0000000000..6d7faab6b7
--- /dev/null
+++ b/mobile/openapi/doc/JobType.md
@@ -0,0 +1,14 @@
+# openapi.model.JobType
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 3c87fc703b..150d878f63 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -31,6 +31,7 @@ part 'api/album_api.dart';
part 'api/asset_api.dart';
part 'api/authentication_api.dart';
part 'api/device_info_api.dart';
+part 'api/job_api.dart';
part 'api/server_info_api.dart';
part 'api/user_api.dart';
@@ -39,6 +40,7 @@ part 'model/add_users_dto.dart';
part 'model/admin_signup_response_dto.dart';
part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart';
+part 'model/all_job_status_response_dto.dart';
part 'model/asset_count_by_time_bucket.dart';
part 'model/asset_count_by_time_bucket_response_dto.dart';
part 'model/asset_count_by_user_id_response_dto.dart';
@@ -61,6 +63,11 @@ part 'model/device_type_enum.dart';
part 'model/exif_response_dto.dart';
part 'model/get_asset_by_time_bucket_dto.dart';
part 'model/get_asset_count_by_time_bucket_dto.dart';
+part 'model/job_command.dart';
+part 'model/job_command_dto.dart';
+part 'model/job_counts.dart';
+part 'model/job_id.dart';
+part 'model/job_status_response_dto.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
diff --git a/mobile/openapi/lib/api/job_api.dart b/mobile/openapi/lib/api/job_api.dart
new file mode 100644
index 0000000000..b64a67c35a
--- /dev/null
+++ b/mobile/openapi/lib/api/job_api.dart
@@ -0,0 +1,159 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+
+class JobApi {
+ JobApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
+
+ final ApiClient apiClient;
+
+ /// Performs an HTTP 'GET /jobs' operation and returns the [Response].
+ Future getAllJobsStatusWithHttpInfo() async {
+ // ignore: prefer_const_declarations
+ final path = r'/jobs';
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ Future getAllJobsStatus() async {
+ final response = await getAllJobsStatusWithHttpInfo();
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AllJobStatusResponseDto',) as AllJobStatusResponseDto;
+
+ }
+ return null;
+ }
+
+ /// Performs an HTTP 'GET /jobs/{jobId}' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [JobId] jobId (required):
+ Future getJobStatusWithHttpInfo(JobId jobId,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/jobs/{jobId}'
+ .replaceAll('{jobId}', jobId.toString());
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [JobId] jobId (required):
+ Future getJobStatus(JobId jobId,) async {
+ final response = await getJobStatusWithHttpInfo(jobId,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'JobStatusResponseDto',) as JobStatusResponseDto;
+
+ }
+ return null;
+ }
+
+ /// Performs an HTTP 'PUT /jobs/{jobId}' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [JobId] jobId (required):
+ ///
+ /// * [JobCommandDto] jobCommandDto (required):
+ Future sendJobCommandWithHttpInfo(JobId jobId, JobCommandDto jobCommandDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/jobs/{jobId}'
+ .replaceAll('{jobId}', jobId.toString());
+
+ // ignore: prefer_final_locals
+ Object? postBody = jobCommandDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'PUT',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [JobId] jobId (required):
+ ///
+ /// * [JobCommandDto] jobCommandDto (required):
+ Future sendJobCommand(JobId jobId, JobCommandDto jobCommandDto,) async {
+ final response = await sendJobCommandWithHttpInfo(jobId, jobCommandDto,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'num',) as num;
+
+ }
+ return null;
+ }
+}
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 13cc028967..827332e9c3 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -202,6 +202,8 @@ class ApiClient {
return AlbumCountResponseDto.fromJson(value);
case 'AlbumResponseDto':
return AlbumResponseDto.fromJson(value);
+ case 'AllJobStatusResponseDto':
+ return AllJobStatusResponseDto.fromJson(value);
case 'AssetCountByTimeBucket':
return AssetCountByTimeBucket.fromJson(value);
case 'AssetCountByTimeBucketResponseDto':
@@ -246,6 +248,16 @@ class ApiClient {
return GetAssetByTimeBucketDto.fromJson(value);
case 'GetAssetCountByTimeBucketDto':
return GetAssetCountByTimeBucketDto.fromJson(value);
+ case 'JobCommand':
+ return JobCommandTypeTransformer().decode(value);
+ case 'JobCommandDto':
+ return JobCommandDto.fromJson(value);
+ case 'JobCounts':
+ return JobCounts.fromJson(value);
+ case 'JobId':
+ return JobIdTypeTransformer().decode(value);
+ case 'JobStatusResponseDto':
+ return JobStatusResponseDto.fromJson(value);
case 'LoginCredentialDto':
return LoginCredentialDto.fromJson(value);
case 'LoginResponseDto':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 26c90fd0c7..7db37768c4 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -64,6 +64,12 @@ String parameterToString(dynamic value) {
if (value is DeviceTypeEnum) {
return DeviceTypeEnumTypeTransformer().encode(value).toString();
}
+ if (value is JobCommand) {
+ return JobCommandTypeTransformer().encode(value).toString();
+ }
+ if (value is JobId) {
+ return JobIdTypeTransformer().encode(value).toString();
+ }
if (value is ThumbnailFormat) {
return ThumbnailFormatTypeTransformer().encode(value).toString();
}
diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart
new file mode 100644
index 0000000000..7be7166a77
--- /dev/null
+++ b/mobile/openapi/lib/model/all_job_status_response_dto.dart
@@ -0,0 +1,167 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class AllJobStatusResponseDto {
+ /// Returns a new [AllJobStatusResponseDto] instance.
+ AllJobStatusResponseDto({
+ required this.thumbnailGenerationQueueCount,
+ required this.metadataExtractionQueueCount,
+ required this.videoConversionQueueCount,
+ required this.machineLearningQueueCount,
+ required this.isThumbnailGenerationActive,
+ required this.isMetadataExtractionActive,
+ required this.isVideoConversionActive,
+ required this.isMachineLearningActive,
+ });
+
+ JobCounts thumbnailGenerationQueueCount;
+
+ JobCounts metadataExtractionQueueCount;
+
+ JobCounts videoConversionQueueCount;
+
+ JobCounts machineLearningQueueCount;
+
+ bool isThumbnailGenerationActive;
+
+ bool isMetadataExtractionActive;
+
+ bool isVideoConversionActive;
+
+ bool isMachineLearningActive;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is AllJobStatusResponseDto &&
+ other.thumbnailGenerationQueueCount == thumbnailGenerationQueueCount &&
+ other.metadataExtractionQueueCount == metadataExtractionQueueCount &&
+ other.videoConversionQueueCount == videoConversionQueueCount &&
+ other.machineLearningQueueCount == machineLearningQueueCount &&
+ other.isThumbnailGenerationActive == isThumbnailGenerationActive &&
+ other.isMetadataExtractionActive == isMetadataExtractionActive &&
+ other.isVideoConversionActive == isVideoConversionActive &&
+ other.isMachineLearningActive == isMachineLearningActive;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (thumbnailGenerationQueueCount.hashCode) +
+ (metadataExtractionQueueCount.hashCode) +
+ (videoConversionQueueCount.hashCode) +
+ (machineLearningQueueCount.hashCode) +
+ (isThumbnailGenerationActive.hashCode) +
+ (isMetadataExtractionActive.hashCode) +
+ (isVideoConversionActive.hashCode) +
+ (isMachineLearningActive.hashCode);
+
+ @override
+ String toString() => 'AllJobStatusResponseDto[thumbnailGenerationQueueCount=$thumbnailGenerationQueueCount, metadataExtractionQueueCount=$metadataExtractionQueueCount, videoConversionQueueCount=$videoConversionQueueCount, machineLearningQueueCount=$machineLearningQueueCount, isThumbnailGenerationActive=$isThumbnailGenerationActive, isMetadataExtractionActive=$isMetadataExtractionActive, isVideoConversionActive=$isVideoConversionActive, isMachineLearningActive=$isMachineLearningActive]';
+
+ Map toJson() {
+ final _json = {};
+ _json[r'thumbnailGenerationQueueCount'] = thumbnailGenerationQueueCount;
+ _json[r'metadataExtractionQueueCount'] = metadataExtractionQueueCount;
+ _json[r'videoConversionQueueCount'] = videoConversionQueueCount;
+ _json[r'machineLearningQueueCount'] = machineLearningQueueCount;
+ _json[r'isThumbnailGenerationActive'] = isThumbnailGenerationActive;
+ _json[r'isMetadataExtractionActive'] = isMetadataExtractionActive;
+ _json[r'isVideoConversionActive'] = isVideoConversionActive;
+ _json[r'isMachineLearningActive'] = isMachineLearningActive;
+ return _json;
+ }
+
+ /// Returns a new [AllJobStatusResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static AllJobStatusResponseDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ // Ensure that the map contains the required keys.
+ // Note 1: the values aren't checked for validity beyond being non-null.
+ // Note 2: this code is stripped in release mode!
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "AllJobStatusResponseDto[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "AllJobStatusResponseDto[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
+
+ return AllJobStatusResponseDto(
+ thumbnailGenerationQueueCount: JobCounts.fromJson(json[r'thumbnailGenerationQueueCount'])!,
+ metadataExtractionQueueCount: JobCounts.fromJson(json[r'metadataExtractionQueueCount'])!,
+ videoConversionQueueCount: JobCounts.fromJson(json[r'videoConversionQueueCount'])!,
+ machineLearningQueueCount: JobCounts.fromJson(json[r'machineLearningQueueCount'])!,
+ isThumbnailGenerationActive: mapValueOfType(json, r'isThumbnailGenerationActive')!,
+ isMetadataExtractionActive: mapValueOfType(json, r'isMetadataExtractionActive')!,
+ isVideoConversionActive: mapValueOfType(json, r'isVideoConversionActive')!,
+ isMachineLearningActive: mapValueOfType(json, r'isMachineLearningActive')!,
+ );
+ }
+ return null;
+ }
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = AllJobStatusResponseDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = AllJobStatusResponseDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of AllJobStatusResponseDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = AllJobStatusResponseDto.listFromJson(entry.value, growable: growable,);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'thumbnailGenerationQueueCount',
+ 'metadataExtractionQueueCount',
+ 'videoConversionQueueCount',
+ 'machineLearningQueueCount',
+ 'isThumbnailGenerationActive',
+ 'isMetadataExtractionActive',
+ 'isVideoConversionActive',
+ 'isMachineLearningActive',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart
index 21dcce7595..cd1e83c5f2 100644
--- a/mobile/openapi/lib/model/asset_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_response_dto.dart
@@ -76,72 +76,69 @@ class AssetResponseDto {
SmartInfoResponseDto? smartInfo;
@override
- bool operator ==(Object other) =>
- identical(this, other) ||
- other is AssetResponseDto &&
- other.type == type &&
- other.id == id &&
- other.deviceAssetId == deviceAssetId &&
- other.ownerId == ownerId &&
- other.deviceId == deviceId &&
- other.originalPath == originalPath &&
- other.resizePath == resizePath &&
- other.createdAt == createdAt &&
- other.modifiedAt == modifiedAt &&
- other.isFavorite == isFavorite &&
- other.mimeType == mimeType &&
- other.duration == duration &&
- other.webpPath == webpPath &&
- other.encodedVideoPath == encodedVideoPath &&
- other.exifInfo == exifInfo &&
- other.smartInfo == smartInfo;
+ bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
+ other.type == type &&
+ other.id == id &&
+ other.deviceAssetId == deviceAssetId &&
+ other.ownerId == ownerId &&
+ other.deviceId == deviceId &&
+ other.originalPath == originalPath &&
+ other.resizePath == resizePath &&
+ other.createdAt == createdAt &&
+ other.modifiedAt == modifiedAt &&
+ other.isFavorite == isFavorite &&
+ other.mimeType == mimeType &&
+ other.duration == duration &&
+ other.webpPath == webpPath &&
+ other.encodedVideoPath == encodedVideoPath &&
+ other.exifInfo == exifInfo &&
+ other.smartInfo == smartInfo;
@override
int get hashCode =>
- // ignore: unnecessary_parenthesis
- (type.hashCode) +
- (id.hashCode) +
- (deviceAssetId.hashCode) +
- (ownerId.hashCode) +
- (deviceId.hashCode) +
- (originalPath.hashCode) +
- (resizePath == null ? 0 : resizePath!.hashCode) +
- (createdAt.hashCode) +
- (modifiedAt.hashCode) +
- (isFavorite.hashCode) +
- (mimeType == null ? 0 : mimeType!.hashCode) +
- (duration.hashCode) +
- (webpPath == null ? 0 : webpPath!.hashCode) +
- (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
- (exifInfo == null ? 0 : exifInfo!.hashCode) +
- (smartInfo == null ? 0 : smartInfo!.hashCode);
+ // ignore: unnecessary_parenthesis
+ (type.hashCode) +
+ (id.hashCode) +
+ (deviceAssetId.hashCode) +
+ (ownerId.hashCode) +
+ (deviceId.hashCode) +
+ (originalPath.hashCode) +
+ (resizePath == null ? 0 : resizePath!.hashCode) +
+ (createdAt.hashCode) +
+ (modifiedAt.hashCode) +
+ (isFavorite.hashCode) +
+ (mimeType == null ? 0 : mimeType!.hashCode) +
+ (duration.hashCode) +
+ (webpPath == null ? 0 : webpPath!.hashCode) +
+ (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
+ (exifInfo == null ? 0 : exifInfo!.hashCode) +
+ (smartInfo == null ? 0 : smartInfo!.hashCode);
@override
- String toString() =>
- 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
+ String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo]';
Map toJson() {
final _json = {};
- _json[r'type'] = type;
- _json[r'id'] = id;
- _json[r'deviceAssetId'] = deviceAssetId;
- _json[r'ownerId'] = ownerId;
- _json[r'deviceId'] = deviceId;
- _json[r'originalPath'] = originalPath;
+ _json[r'type'] = type;
+ _json[r'id'] = id;
+ _json[r'deviceAssetId'] = deviceAssetId;
+ _json[r'ownerId'] = ownerId;
+ _json[r'deviceId'] = deviceId;
+ _json[r'originalPath'] = originalPath;
if (resizePath != null) {
_json[r'resizePath'] = resizePath;
} else {
_json[r'resizePath'] = null;
}
- _json[r'createdAt'] = createdAt;
- _json[r'modifiedAt'] = modifiedAt;
- _json[r'isFavorite'] = isFavorite;
+ _json[r'createdAt'] = createdAt;
+ _json[r'modifiedAt'] = modifiedAt;
+ _json[r'isFavorite'] = isFavorite;
if (mimeType != null) {
_json[r'mimeType'] = mimeType;
} else {
_json[r'mimeType'] = null;
}
- _json[r'duration'] = duration;
+ _json[r'duration'] = duration;
if (webpPath != null) {
_json[r'webpPath'] = webpPath;
} else {
@@ -175,13 +172,13 @@ class AssetResponseDto {
// Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode!
- // assert(() {
- // requiredKeys.forEach((key) {
- // assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
- // assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
- // });
- // return true;
- // }());
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
return AssetResponseDto(
type: AssetTypeEnum.fromJson(json[r'type'])!,
@@ -205,10 +202,7 @@ class AssetResponseDto {
return null;
}
- static List? listFromJson(
- dynamic json, {
- bool growable = false,
- }) {
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
final result = [];
if (json is List && json.isNotEmpty) {
for (final row in json) {
@@ -236,18 +230,12 @@ class AssetResponseDto {
}
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
- static Map> mapListFromJson(
- dynamic json, {
- bool growable = false,
- }) {
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = >{};
if (json is Map && json.isNotEmpty) {
json = json.cast(); // ignore: parameter_assignments
for (final entry in json.entries) {
- final value = AssetResponseDto.listFromJson(
- entry.value,
- growable: growable,
- );
+ final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
if (value != null) {
map[entry.key] = value;
}
@@ -274,3 +262,4 @@ class AssetResponseDto {
'encodedVideoPath',
};
}
+
diff --git a/mobile/openapi/lib/model/create_job_dto.dart b/mobile/openapi/lib/model/create_job_dto.dart
new file mode 100644
index 0000000000..1eaf678647
--- /dev/null
+++ b/mobile/openapi/lib/model/create_job_dto.dart
@@ -0,0 +1,111 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class CreateJobDto {
+ /// Returns a new [CreateJobDto] instance.
+ CreateJobDto({
+ required this.jobType,
+ });
+
+ JobType jobType;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is CreateJobDto &&
+ other.jobType == jobType;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (jobType.hashCode);
+
+ @override
+ String toString() => 'CreateJobDto[jobType=$jobType]';
+
+ Map toJson() {
+ final _json = {};
+ _json[r'jobType'] = jobType;
+ return _json;
+ }
+
+ /// Returns a new [CreateJobDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static CreateJobDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ // Ensure that the map contains the required keys.
+ // Note 1: the values aren't checked for validity beyond being non-null.
+ // Note 2: this code is stripped in release mode!
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "CreateJobDto[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "CreateJobDto[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
+
+ return CreateJobDto(
+ jobType: JobType.fromJson(json[r'jobType'])!,
+ );
+ }
+ return null;
+ }
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = CreateJobDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = CreateJobDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of CreateJobDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = CreateJobDto.listFromJson(entry.value, growable: growable,);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'jobType',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart
index 199c955e93..b81f0e347b 100644
--- a/mobile/openapi/lib/model/exif_response_dto.dart
+++ b/mobile/openapi/lib/model/exif_response_dto.dart
@@ -14,12 +14,12 @@ class ExifResponseDto {
/// Returns a new [ExifResponseDto] instance.
ExifResponseDto({
this.id,
+ this.fileSizeInByte,
this.make,
this.model,
this.imageName,
this.exifImageWidth,
this.exifImageHeight,
- this.fileSizeInByte,
this.orientation,
this.dateTimeOriginal,
this.modifyDate,
@@ -35,7 +35,9 @@ class ExifResponseDto {
this.country,
});
- String? id;
+ int? id;
+
+ int? fileSizeInByte;
String? make;
@@ -47,8 +49,6 @@ class ExifResponseDto {
num? exifImageHeight;
- num? fileSizeInByte;
-
String? orientation;
DateTime? dateTimeOriginal;
@@ -78,12 +78,12 @@ class ExifResponseDto {
@override
bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
other.id == id &&
+ other.fileSizeInByte == fileSizeInByte &&
other.make == make &&
other.model == model &&
other.imageName == imageName &&
other.exifImageWidth == exifImageWidth &&
other.exifImageHeight == exifImageHeight &&
- other.fileSizeInByte == fileSizeInByte &&
other.orientation == orientation &&
other.dateTimeOriginal == dateTimeOriginal &&
other.modifyDate == modifyDate &&
@@ -102,12 +102,12 @@ class ExifResponseDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(id == null ? 0 : id!.hashCode) +
+ (fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
(make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) +
(imageName == null ? 0 : imageName!.hashCode) +
(exifImageWidth == null ? 0 : exifImageWidth!.hashCode) +
(exifImageHeight == null ? 0 : exifImageHeight!.hashCode) +
- (fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
(orientation == null ? 0 : orientation!.hashCode) +
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(modifyDate == null ? 0 : modifyDate!.hashCode) +
@@ -123,7 +123,7 @@ class ExifResponseDto {
(country == null ? 0 : country!.hashCode);
@override
- String toString() => 'ExifResponseDto[id=$id, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, fileSizeInByte=$fileSizeInByte, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
+ String toString() => 'ExifResponseDto[id=$id, fileSizeInByte=$fileSizeInByte, make=$make, model=$model, imageName=$imageName, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]';
Map toJson() {
final _json = {};
@@ -132,6 +132,11 @@ class ExifResponseDto {
} else {
_json[r'id'] = null;
}
+ if (fileSizeInByte != null) {
+ _json[r'fileSizeInByte'] = fileSizeInByte;
+ } else {
+ _json[r'fileSizeInByte'] = null;
+ }
if (make != null) {
_json[r'make'] = make;
} else {
@@ -157,11 +162,6 @@ class ExifResponseDto {
} else {
_json[r'exifImageHeight'] = null;
}
- if (fileSizeInByte != null) {
- _json[r'fileSizeInByte'] = fileSizeInByte;
- } else {
- _json[r'fileSizeInByte'] = null;
- }
if (orientation != null) {
_json[r'orientation'] = orientation;
} else {
@@ -249,7 +249,8 @@ class ExifResponseDto {
}());
return ExifResponseDto(
- id: mapValueOfType(json, r'id'),
+ id: mapValueOfType(json, r'id'),
+ fileSizeInByte: mapValueOfType(json, r'fileSizeInByte'),
make: mapValueOfType(json, r'make'),
model: mapValueOfType(json, r'model'),
imageName: mapValueOfType(json, r'imageName'),
@@ -259,9 +260,6 @@ class ExifResponseDto {
exifImageHeight: json[r'exifImageHeight'] == null
? null
: num.parse(json[r'exifImageHeight'].toString()),
- fileSizeInByte: json[r'fileSizeInByte'] == null
- ? null
- : num.parse(json[r'fileSizeInByte'].toString()),
orientation: mapValueOfType(json, r'orientation'),
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', ''),
modifyDate: mapDateTime(json, r'modifyDate', ''),
diff --git a/mobile/openapi/lib/model/job_command.dart b/mobile/openapi/lib/model/job_command.dart
new file mode 100644
index 0000000000..2734028076
--- /dev/null
+++ b/mobile/openapi/lib/model/job_command.dart
@@ -0,0 +1,85 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+
+class JobCommand {
+ /// Instantiate a new enum with the provided [value].
+ const JobCommand._(this.value);
+
+ /// The underlying value of this enum member.
+ final String value;
+
+ @override
+ String toString() => value;
+
+ String toJson() => value;
+
+ static const start = JobCommand._(r'start');
+ static const stop = JobCommand._(r'stop');
+
+ /// List of all possible values in this [enum][JobCommand].
+ static const values = [
+ start,
+ stop,
+ ];
+
+ static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = JobCommand.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+}
+
+/// Transformation class that can [encode] an instance of [JobCommand] to String,
+/// and [decode] dynamic data back to [JobCommand].
+class JobCommandTypeTransformer {
+ factory JobCommandTypeTransformer() => _instance ??= const JobCommandTypeTransformer._();
+
+ const JobCommandTypeTransformer._();
+
+ String encode(JobCommand data) => data.value;
+
+ /// Decodes a [dynamic value][data] to a JobCommand.
+ ///
+ /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+ /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+ /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+ ///
+ /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+ /// and users are still using an old app with the old code.
+ JobCommand? decode(dynamic data, {bool allowNull = true}) {
+ if (data != null) {
+ switch (data.toString()) {
+ case r'start': return JobCommand.start;
+ case r'stop': return JobCommand.stop;
+ default:
+ if (!allowNull) {
+ throw ArgumentError('Unknown enum value to decode: $data');
+ }
+ }
+ }
+ return null;
+ }
+
+ /// Singleton [JobCommandTypeTransformer] instance.
+ static JobCommandTypeTransformer? _instance;
+}
+
diff --git a/mobile/openapi/lib/model/job_command_dto.dart b/mobile/openapi/lib/model/job_command_dto.dart
new file mode 100644
index 0000000000..808eb50d75
--- /dev/null
+++ b/mobile/openapi/lib/model/job_command_dto.dart
@@ -0,0 +1,111 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class JobCommandDto {
+ /// Returns a new [JobCommandDto] instance.
+ JobCommandDto({
+ required this.command,
+ });
+
+ JobCommand command;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is JobCommandDto &&
+ other.command == command;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (command.hashCode);
+
+ @override
+ String toString() => 'JobCommandDto[command=$command]';
+
+ Map toJson() {
+ final _json = {};
+ _json[r'command'] = command;
+ return _json;
+ }
+
+ /// Returns a new [JobCommandDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static JobCommandDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ // Ensure that the map contains the required keys.
+ // Note 1: the values aren't checked for validity beyond being non-null.
+ // Note 2: this code is stripped in release mode!
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "JobCommandDto[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "JobCommandDto[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
+
+ return JobCommandDto(
+ command: JobCommand.fromJson(json[r'command'])!,
+ );
+ }
+ return null;
+ }
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = JobCommandDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = JobCommandDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of JobCommandDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = JobCommandDto.listFromJson(entry.value, growable: growable,);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'command',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/job_counts.dart b/mobile/openapi/lib/model/job_counts.dart
new file mode 100644
index 0000000000..dadb72f328
--- /dev/null
+++ b/mobile/openapi/lib/model/job_counts.dart
@@ -0,0 +1,143 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class JobCounts {
+ /// Returns a new [JobCounts] instance.
+ JobCounts({
+ required this.active,
+ required this.completed,
+ required this.failed,
+ required this.delayed,
+ required this.waiting,
+ });
+
+ int active;
+
+ int completed;
+
+ int failed;
+
+ int delayed;
+
+ int waiting;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is JobCounts &&
+ other.active == active &&
+ other.completed == completed &&
+ other.failed == failed &&
+ other.delayed == delayed &&
+ other.waiting == waiting;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (active.hashCode) +
+ (completed.hashCode) +
+ (failed.hashCode) +
+ (delayed.hashCode) +
+ (waiting.hashCode);
+
+ @override
+ String toString() => 'JobCounts[active=$active, completed=$completed, failed=$failed, delayed=$delayed, waiting=$waiting]';
+
+ Map toJson() {
+ final _json = {};
+ _json[r'active'] = active;
+ _json[r'completed'] = completed;
+ _json[r'failed'] = failed;
+ _json[r'delayed'] = delayed;
+ _json[r'waiting'] = waiting;
+ return _json;
+ }
+
+ /// Returns a new [JobCounts] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static JobCounts? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ // Ensure that the map contains the required keys.
+ // Note 1: the values aren't checked for validity beyond being non-null.
+ // Note 2: this code is stripped in release mode!
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "JobCounts[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "JobCounts[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
+
+ return JobCounts(
+ active: mapValueOfType(json, r'active')!,
+ completed: mapValueOfType(json, r'completed')!,
+ failed: mapValueOfType(json, r'failed')!,
+ delayed: mapValueOfType(json, r'delayed')!,
+ waiting: mapValueOfType(json, r'waiting')!,
+ );
+ }
+ return null;
+ }
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = JobCounts.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = JobCounts.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of JobCounts-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = JobCounts.listFromJson(entry.value, growable: growable,);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'active',
+ 'completed',
+ 'failed',
+ 'delayed',
+ 'waiting',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/job_id.dart b/mobile/openapi/lib/model/job_id.dart
new file mode 100644
index 0000000000..308d9c06c1
--- /dev/null
+++ b/mobile/openapi/lib/model/job_id.dart
@@ -0,0 +1,91 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+
+class JobId {
+ /// Instantiate a new enum with the provided [value].
+ const JobId._(this.value);
+
+ /// The underlying value of this enum member.
+ final String value;
+
+ @override
+ String toString() => value;
+
+ String toJson() => value;
+
+ static const thumbnailGeneration = JobId._(r'thumbnail-generation');
+ static const metadataExtraction = JobId._(r'metadata-extraction');
+ static const videoConversion = JobId._(r'video-conversion');
+ static const machineLearning = JobId._(r'machine-learning');
+
+ /// List of all possible values in this [enum][JobId].
+ static const values = [
+ thumbnailGeneration,
+ metadataExtraction,
+ videoConversion,
+ machineLearning,
+ ];
+
+ static JobId? fromJson(dynamic value) => JobIdTypeTransformer().decode(value);
+
+ static List? listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = JobId.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+}
+
+/// Transformation class that can [encode] an instance of [JobId] to String,
+/// and [decode] dynamic data back to [JobId].
+class JobIdTypeTransformer {
+ factory JobIdTypeTransformer() => _instance ??= const JobIdTypeTransformer._();
+
+ const JobIdTypeTransformer._();
+
+ String encode(JobId data) => data.value;
+
+ /// Decodes a [dynamic value][data] to a JobId.
+ ///
+ /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
+ /// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
+ /// cannot be decoded successfully, then an [UnimplementedError] is thrown.
+ ///
+ /// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
+ /// and users are still using an old app with the old code.
+ JobId? decode(dynamic data, {bool allowNull = true}) {
+ if (data != null) {
+ switch (data.toString()) {
+ case r'thumbnail-generation': return JobId.thumbnailGeneration;
+ case r'metadata-extraction': return JobId.metadataExtraction;
+ case r'video-conversion': return JobId.videoConversion;
+ case r'machine-learning': return JobId.machineLearning;
+ default:
+ if (!allowNull) {
+ throw ArgumentError('Unknown enum value to decode: $data');
+ }
+ }
+ }
+ return null;
+ }
+
+ /// Singleton [JobIdTypeTransformer] instance.
+ static JobIdTypeTransformer? _instance;
+}
+
diff --git a/mobile/openapi/lib/model/job_status_response_dto.dart b/mobile/openapi/lib/model/job_status_response_dto.dart
new file mode 100644
index 0000000000..d3854b8f3a
--- /dev/null
+++ b/mobile/openapi/lib/model/job_status_response_dto.dart
@@ -0,0 +1,119 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class JobStatusResponseDto {
+ /// Returns a new [JobStatusResponseDto] instance.
+ JobStatusResponseDto({
+ required this.isActive,
+ required this.queueCount,
+ });
+
+ bool isActive;
+
+ Object queueCount;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is JobStatusResponseDto &&
+ other.isActive == isActive &&
+ other.queueCount == queueCount;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (isActive.hashCode) +
+ (queueCount.hashCode);
+
+ @override
+ String toString() => 'JobStatusResponseDto[isActive=$isActive, queueCount=$queueCount]';
+
+ Map toJson() {
+ final _json = {};
+ _json[r'isActive'] = isActive;
+ _json[r'queueCount'] = queueCount;
+ return _json;
+ }
+
+ /// Returns a new [JobStatusResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static JobStatusResponseDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ // Ensure that the map contains the required keys.
+ // Note 1: the values aren't checked for validity beyond being non-null.
+ // Note 2: this code is stripped in release mode!
+ assert(() {
+ requiredKeys.forEach((key) {
+ assert(json.containsKey(key), 'Required key "JobStatusResponseDto[$key]" is missing from JSON.');
+ assert(json[key] != null, 'Required key "JobStatusResponseDto[$key]" has a null value in JSON.');
+ });
+ return true;
+ }());
+
+ return JobStatusResponseDto(
+ isActive: mapValueOfType(json, r'isActive')!,
+ queueCount: mapValueOfType