Compare commits
39 Commits
v1.29.2_43
...
v1.31.0_49
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
471a60dcb0 | ||
|
|
46994c3355 | ||
|
|
642811869c | ||
|
|
3be4697487 | ||
|
|
a3aca4acb5 | ||
|
|
7587f858ae | ||
|
|
854c214bc0 | ||
|
|
5dfce4db34 | ||
|
|
95467fa3c1 | ||
|
|
4ec3453558 | ||
|
|
536fda04f2 | ||
|
|
2094204877 | ||
|
|
ab375cca1a | ||
|
|
479f706f8a | ||
|
|
4342285507 | ||
|
|
8bb656cb17 | ||
|
|
3f1f835df3 | ||
|
|
87ca031335 | ||
|
|
96b9e37461 | ||
|
|
0d3a2fe844 | ||
|
|
848781aef5 | ||
|
|
28bf497a0b | ||
|
|
8ede738396 | ||
|
|
40c2b6a563 | ||
|
|
3581cf7305 | ||
|
|
c33775b944 | ||
|
|
b0cd2522e0 | ||
|
|
c3979f6e31 | ||
|
|
103df4d9f3 | ||
|
|
040e02cfc5 | ||
|
|
f377b64065 | ||
|
|
e5459b68ff | ||
|
|
fc194021a4 | ||
|
|
39f8ca3bf1 | ||
|
|
7a807f7216 | ||
|
|
bedfb51b1c | ||
|
|
b2afb95c19 | ||
|
|
10239161fd | ||
|
|
242f10952d |
@@ -2,8 +2,6 @@ name: Build and Push Docker Image - Staging
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
@@ -38,6 +36,7 @@ jobs:
|
|||||||
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-server:staging
|
altran1502/immich-server:staging
|
||||||
|
altran1502/immich-server:${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
build_and_push_machine_learning_staging:
|
build_and_push_machine_learning_staging:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -67,6 +66,7 @@ jobs:
|
|||||||
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-machine-learning:staging
|
altran1502/immich-machine-learning:staging
|
||||||
|
altran1502/immich-machine-learning:${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
build_and_push_web_staging:
|
build_and_push_web_staging:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -96,6 +96,7 @@ jobs:
|
|||||||
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-web:staging
|
altran1502/immich-web:staging
|
||||||
|
altran1502/immich-web:${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
build_and_push_nginx_staging:
|
build_and_push_nginx_staging:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -124,3 +125,4 @@ jobs:
|
|||||||
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }}
|
||||||
tags: |
|
tags: |
|
||||||
altran1502/immich-proxy:staging
|
altran1502/immich-proxy:staging
|
||||||
|
altran1502/immich-proxy:${{ github.event.pull_request.number }}
|
||||||
|
|||||||
74
.github/workflows/codeql-analysis.yml
vendored
Normal file
74
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '20 13 * * 1'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'javascript', 'python' ]
|
||||||
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
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
|
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:
|
server-unit-tests:
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -23,12 +23,30 @@
|
|||||||
<br/>
|
<br/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
You can access the web demo at https://demo.immich.app
|
||||||
|
|
||||||
|
For the mobile app, you can use https://demo.immich.app/api for the `Server Endpoint URL`
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
The credential
|
||||||
|
email: demo@immich.app
|
||||||
|
password: demo
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||||
|
```
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Screenshots](#screenshots)
|
- [Screenshots](#screenshots)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
- [Update](#update)
|
- [Update](#update)
|
||||||
- [Mobile App](#-mobile-app)
|
- [Mobile App](#mobile-app)
|
||||||
|
- [App Beta Invitation links](#App-Beta-release-channel)
|
||||||
- [Development](#development)
|
- [Development](#development)
|
||||||
- [Support](#support)
|
- [Support](#support)
|
||||||
- [Known Issues](#known-issues)
|
- [Known Issues](#known-issues)
|
||||||
@@ -146,8 +164,6 @@ wget -O .env https://raw.githubusercontent.com/immich-app/immich/main/docker/.en
|
|||||||
* Populate custom database information if necessary.
|
* Populate custom database information if necessary.
|
||||||
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
* Populate `UPLOAD_LOCATION` as prefered location for storing backup assets.
|
||||||
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
* Populate a secret value for `JWT_SECRET`, you can use this command: `openssl rand -base64 128`
|
||||||
* [Optional] Populate Mapbox value to use reverse geocoding.
|
|
||||||
* [Optional] Populate `TZ` as your timezone, default is `Etc/UTC`.
|
|
||||||
|
|
||||||
### Step 3 - Start the containers
|
### Step 3 - Start the containers
|
||||||
|
|
||||||
@@ -190,7 +206,11 @@ 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 the 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)
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ DB_DATABASE_NAME=immich
|
|||||||
# Optional Database settings:
|
# Optional Database settings:
|
||||||
# DB_PORT=5432
|
# DB_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Redis
|
# Redis
|
||||||
###################################################################################
|
###################################################################################
|
||||||
@@ -25,41 +22,39 @@ REDIS_HOSTNAME=immich_redis
|
|||||||
# REDIS_PASSWORD=
|
# REDIS_PASSWORD=
|
||||||
# REDIS_SOCKET=
|
# REDIS_SOCKET=
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Upload File Config
|
# Upload File Config
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# Log message level - [simple|verbose]
|
# Log message level - [simple|verbose]
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
LOG_LEVEL=simple
|
LOG_LEVEL=simple
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# JWT SECRET
|
# JWT SECRET
|
||||||
###################################################################################
|
###################################################################################
|
||||||
|
|
||||||
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###################################################################################
|
###################################################################################
|
||||||
# MAPBOX
|
# Reverse Geocoding
|
||||||
####################################################################################
|
####################################################################################
|
||||||
|
|
||||||
# ENABLE_MAPBOX is either true of false -> if true, you have to provide MAPBOX_KEY
|
# DISABLE_REVERSE_GEOCODING=false
|
||||||
ENABLE_MAPBOX=false
|
|
||||||
MAPBOX_KEY=
|
|
||||||
|
|
||||||
|
# Reverse geocoding is done locally which has a small impact on memory usage
|
||||||
|
# This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable
|
||||||
|
# This ranges from 0-3 with 3 being the most precise
|
||||||
|
# 3 - Cities > 500 population: ~200MB RAM
|
||||||
|
# 2 - Cities > 1000 population: ~150MB RAM
|
||||||
|
# 1 - Cities > 5000 population: ~80MB RAM
|
||||||
|
# 0 - Cities > 15000 population: ~40MB RAM
|
||||||
|
|
||||||
|
# REVERSE_GEOCODING_PRECISION=3
|
||||||
|
|
||||||
####################################################################################
|
####################################################################################
|
||||||
# WEB - Optional
|
# WEB - Optional
|
||||||
@@ -69,11 +64,3 @@ MAPBOX_KEY=
|
|||||||
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
# For example PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
|
||||||
|
|
||||||
PUBLIC_LOGIN_PAGE_MESSAGE=
|
PUBLIC_LOGIN_PAGE_MESSAGE=
|
||||||
|
|
||||||
# For correctly display your local time zone on the web, you can set the time zone here.
|
|
||||||
# Should work fine by default value, however, in case of incorrect timezone in EXIF, this value
|
|
||||||
# should be set to the correct timezone.
|
|
||||||
# Command to get timezone:
|
|
||||||
# - Linux: curl -s http://ip-api.com/json/ | grep -oP '(?<=timezone":")(.*?)(?=")'
|
|
||||||
|
|
||||||
# TZ=Etc/UTC
|
|
||||||
@@ -47,8 +47,6 @@ services:
|
|||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
|
||||||
- PUBLIC_TZ=${TZ}
|
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
|
|||||||
2227
machine-learning/package-lock.json
generated
2227
machine-learning/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,6 @@
|
|||||||
"@nestjs/core": "^8.0.0",
|
"@nestjs/core": "^8.0.0",
|
||||||
"@nestjs/mapped-types": "^1.0.1",
|
"@nestjs/mapped-types": "^1.0.1",
|
||||||
"@nestjs/platform-express": "^8.0.0",
|
"@nestjs/platform-express": "^8.0.0",
|
||||||
"@nestjs/typeorm": "^8.0.3",
|
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||||
"@tensorflow-models/mobilenet": "^2.1.0",
|
"@tensorflow-models/mobilenet": "^2.1.0",
|
||||||
"@tensorflow/tfjs": "^3.19.0",
|
"@tensorflow/tfjs": "^3.19.0",
|
||||||
@@ -34,11 +33,9 @@
|
|||||||
"@tensorflow/tfjs-node": "^3.19.0",
|
"@tensorflow/tfjs-node": "^3.19.0",
|
||||||
"@tensorflow/tfjs-node-gpu": "^3.19.0",
|
"@tensorflow/tfjs-node-gpu": "^3.19.0",
|
||||||
"@trpc/server": "^9.20.3",
|
"@trpc/server": "^9.20.3",
|
||||||
"pg": "^8.7.3",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.2.0",
|
"rxjs": "^7.2.0"
|
||||||
"typeorm": "^0.2.45"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^8.2.4",
|
"@nestjs/cli": "^8.2.4",
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
|
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
|
||||||
import { databaseConfig } from './config/database.config';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { ObjectDetectionModule } from './object-detection/object-detection.module';
|
import { ObjectDetectionModule } from './object-detection/object-detection.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [ImageClassifierModule, ObjectDetectionModule],
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
|
||||||
ImageClassifierModule,
|
|
||||||
ObjectDetectionModule,
|
|
||||||
],
|
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
|
|
||||||
export const databaseConfig: TypeOrmModuleOptions = {
|
|
||||||
type: 'postgres',
|
|
||||||
host: process.env.DB_HOSTNAME || 'immich_postgres',
|
|
||||||
port: parseInt(process.env.DB_PORT || '5432'),
|
|
||||||
username: process.env.DB_USERNAME,
|
|
||||||
password: process.env.DB_PASSWORD,
|
|
||||||
database: process.env.DB_DATABASE_NAME,
|
|
||||||
synchronize: false,
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich" xmlns:tools="http://schemas.android.com/tools">
|
||||||
<application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
|
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
|
||||||
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
the Android process has started. This theme is visible to the user
|
the Android process has started. This theme is visible to the user
|
||||||
@@ -12,12 +12,15 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<service android:name=".AppClearedService" android:stopWithTask="false" />
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data android:name="flutterEmbedding" android:value="2" />
|
<meta-data android:name="flutterEmbedding" android:value="2" />
|
||||||
|
<!-- Disables default WorkManager initialization to use our custom initialization -->
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
tools:node="remove">
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
|
|||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val ctx = context!!
|
val ctx = context!!
|
||||||
when(call.method) {
|
when (call.method) {
|
||||||
"enable" -> {
|
"enable" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
|
||||||
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
|
||||||
.apply()
|
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
|
||||||
|
.apply()
|
||||||
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
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 notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
|
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
|
||||||
private var timeBackupStarted: Long = 0L
|
private var timeBackupStarted: Long = 0L
|
||||||
|
private var notificationBuilder: NotificationCompat.Builder? = null
|
||||||
|
private var notificationDetailBuilder: NotificationCompat.Builder? = null
|
||||||
|
|
||||||
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||||
|
|
||||||
@@ -61,16 +64,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
// Create a Notification channel if necessary
|
// Create a Notification channel if necessary
|
||||||
createChannel()
|
createChannel()
|
||||||
}
|
}
|
||||||
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
|
||||||
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
|
||||||
if (isIgnoringBatteryOptimizations) {
|
if (isIgnoringBatteryOptimizations) {
|
||||||
// normal background services can only up to 10 minutes
|
// normal background services can only up to 10 minutes
|
||||||
// foreground services are allowed to run indefinitely
|
// foreground services are allowed to run indefinitely
|
||||||
// requires battery optimizations to be disabled (either manually by the user
|
// requires battery optimizations to be disabled (either manually by the user
|
||||||
// or by the system learning that immich is important to the user)
|
// or by the system learning that immich is important to the user)
|
||||||
setForegroundAsync(createForegroundInfo(title))
|
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||||
} else {
|
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
|
||||||
showBackgroundInfo(title)
|
showInfo(getInfoBuilder(title, indeterminate=true).build())
|
||||||
}
|
}
|
||||||
engine = FlutterEngine(ctx)
|
engine = FlutterEngine(ctx)
|
||||||
|
|
||||||
@@ -154,18 +155,21 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
}
|
}
|
||||||
"updateNotification" -> {
|
"updateNotification" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
val title = args.get(0) as String
|
val title = args.get(0) as String?
|
||||||
val content = args.get(1) as String
|
val content = args.get(1) as String?
|
||||||
if (isIgnoringBatteryOptimizations) {
|
val progress = args.get(2) as Int
|
||||||
setForegroundAsync(createForegroundInfo(title, content))
|
val max = args.get(3) as Int
|
||||||
} else {
|
val indeterminate = args.get(4) as Boolean
|
||||||
showBackgroundInfo(title, content)
|
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" -> {
|
"showError" -> {
|
||||||
val args = call.arguments<ArrayList<*>>()!!
|
val args = call.arguments<ArrayList<*>>()!!
|
||||||
val title = args.get(0) as String
|
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?
|
val individualTag = args.get(2) as String?
|
||||||
showError(title, content, individualTag)
|
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)
|
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setTicker(title)
|
.setTicker(title)
|
||||||
.setContentText(content)
|
.setContentText(content)
|
||||||
.setSmallIcon(R.mipmap.ic_launcher)
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.build()
|
.build()
|
||||||
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
|
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
|
||||||
}
|
}
|
||||||
@@ -197,38 +200,54 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
notificationManager.cancel(NOTIFICATION_ERROR_ID)
|
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() {
|
private fun clearBackgroundNotification() {
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
notificationManager.cancel(NOTIFICATION_DETAIL_ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo {
|
private fun showInfo(notification: Notification, isDetail: Boolean = false) {
|
||||||
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
|
val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
|
||||||
.setContentTitle(title)
|
if (isIgnoringBatteryOptimizations) {
|
||||||
.setTicker(title)
|
setForegroundAsync(ForegroundInfo(id, notification))
|
||||||
.setContentText(content)
|
} else {
|
||||||
.setSmallIcon(R.mipmap.ic_launcher)
|
notificationManager.notify(id, notification)
|
||||||
.setOngoing(true)
|
}
|
||||||
.build()
|
}
|
||||||
return ForegroundInfo(NOTIFICATION_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)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun createChannel() {
|
private fun createChannel() {
|
||||||
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
|
val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
|
||||||
notificationManager.createNotificationChannel(foreground)
|
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)
|
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_DEFAULT_TITLE = "Immich"
|
||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
private const val NOTIFICATION_ERROR_ID = 2
|
private const val NOTIFICATION_ERROR_ID = 2
|
||||||
|
private const val NOTIFICATION_DETAIL_ID = 3
|
||||||
private const val ONE_MINUTE = 60000L
|
private const val ONE_MINUTE = 60000L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,9 +46,6 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
|
|||||||
* @param context Android Context
|
* @param context Android Context
|
||||||
*/
|
*/
|
||||||
fun enable(context: Context, immediate: Boolean = false) {
|
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)
|
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
|
||||||
Log.d(TAG, "enabled ContentObserverWorker")
|
Log.d(TAG, "enabled ContentObserverWorker")
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
@@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
|
|||||||
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
|
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)
|
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 requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
|
||||||
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
|
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
|
||||||
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
|
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,21 +5,11 @@ import io.flutter.embedding.engine.FlutterEngine
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
|
||||||
class MainActivity: FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.getPlugins().add(BackgroundServicePlugin())
|
flutterEngine.plugins.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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,17 @@
|
|||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
desc "Build Android"
|
desc "Build Android and Release Testing"
|
||||||
lane :build do
|
lane :beta do
|
||||||
gradle(
|
gradle(
|
||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
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
|
end
|
||||||
|
|
||||||
desc "Build and Release Android"
|
desc "Build and Release Android"
|
||||||
@@ -30,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 43,
|
"android.injected.version.code" => 49,
|
||||||
"android.injected.version.name" => "1.29.1",
|
"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')
|
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')
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
|
|||||||
|
|
||||||
## Android
|
## Android
|
||||||
|
|
||||||
### android build
|
### android beta
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
[bundle exec] fastlane android build
|
[bundle exec] fastlane android beta
|
||||||
```
|
```
|
||||||
|
|
||||||
Build Android
|
Build Android and Release Testing
|
||||||
|
|
||||||
### android release
|
### android release
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fixed app crashes when there is no object detection result.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Correctly display time based on timezone
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Added improvement for timeline view
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Improve scroll thumb date info
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* Fixed parsing date error prevent timeline to be loaded.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
* Fixed run background service after being killed
|
||||||
|
* Added background backup progress notifications
|
||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00023">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000233">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="58.722434">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="61.699536">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="36.768014">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="46.210553">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,10 @@
|
|||||||
"setting_notifications_notify_never": "never",
|
"setting_notifications_notify_never": "never",
|
||||||
"setting_notifications_subtitle": "Adjust your notification preferences",
|
"setting_notifications_subtitle": "Adjust your notification preferences",
|
||||||
"setting_notifications_title": "Notifications",
|
"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",
|
"setting_pages_app_bar_settings": "Settings",
|
||||||
"share_add": "Add",
|
"share_add": "Add",
|
||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
@@ -165,5 +169,10 @@
|
|||||||
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||||
|
"experimental_settings_title": "Experimental",
|
||||||
|
"experimental_settings_subtitle": "Use at your own risk!",
|
||||||
|
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
|
||||||
|
"experimental_settings_new_asset_list_subtitle": "Work in progress",
|
||||||
|
"settings_require_restart": "Please restart Immich to apply this setting"
|
||||||
}
|
}
|
||||||
@@ -360,7 +360,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 58;
|
CURRENT_PROJECT_VERSION = 62;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -495,7 +495,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 58;
|
CURRENT_PROJECT_VERSION = 62;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -522,7 +522,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 58;
|
CURRENT_PROJECT_VERSION = 62;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -17,11 +17,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.30.0</string>
|
<string>1.30.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>58</string>
|
<string>62</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true />
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.29.1"
|
version_number: "1.31.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000173">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.412813">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.78333">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.69289">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.947588">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.408563">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.505399">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="65.350555">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="80.954627">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.894733">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="58.295965">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class ExifBottomSheet extends ConsumerWidget {
|
|||||||
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
if (assetDetail.exifInfo?.dateTimeOriginal != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat('date_format'.tr()).format(
|
DateFormat('date_format'.tr()).format(
|
||||||
assetDetail.exifInfo!.dateTimeOriginal!,
|
assetDetail.exifInfo!.dateTimeOriginal!.toLocal(),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.grey[400],
|
color: Colors.grey[400],
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ final backgroundServiceProvider = Provider(
|
|||||||
/// Background backup service
|
/// Background backup service
|
||||||
class BackgroundService {
|
class BackgroundService {
|
||||||
static const String _portNameLock = "immichLock";
|
static const String _portNameLock = "immichLock";
|
||||||
BackgroundService();
|
|
||||||
static const MethodChannel _foregroundChannel =
|
static const MethodChannel _foregroundChannel =
|
||||||
MethodChannel('immich/foregroundChannel');
|
MethodChannel('immich/foregroundChannel');
|
||||||
static const MethodChannel _backgroundChannel =
|
static const MethodChannel _backgroundChannel =
|
||||||
MethodChannel('immich/backgroundChannel');
|
MethodChannel('immich/backgroundChannel');
|
||||||
|
static final NumberFormat numberFormat = NumberFormat("###0.##");
|
||||||
bool _isBackgroundInitialized = false;
|
bool _isBackgroundInitialized = false;
|
||||||
CancellationToken? _cancellationToken;
|
CancellationToken? _cancellationToken;
|
||||||
bool _canceledBySystem = false;
|
bool _canceledBySystem = false;
|
||||||
@@ -40,6 +40,10 @@ class BackgroundService {
|
|||||||
SendPort? _waitingIsolate;
|
SendPort? _waitingIsolate;
|
||||||
ReceivePort? _rp;
|
ReceivePort? _rp;
|
||||||
bool _errorGracePeriodExceeded = true;
|
bool _errorGracePeriodExceeded = true;
|
||||||
|
int _uploadedAssetsCount = 0;
|
||||||
|
int _assetsToUploadCount = 0;
|
||||||
|
int _lastDetailProgressUpdate = 0;
|
||||||
|
String _lastPrintedProgress = "";
|
||||||
|
|
||||||
bool get isBackgroundInitialized {
|
bool get isBackgroundInitialized {
|
||||||
return _isBackgroundInitialized;
|
return _isBackgroundInitialized;
|
||||||
@@ -125,22 +129,29 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the notification shown by the background service
|
/// Updates the notification shown by the background service
|
||||||
Future<bool> _updateNotification({
|
Future<bool?> _updateNotification({
|
||||||
required String title,
|
String? title,
|
||||||
String? content,
|
String? content,
|
||||||
|
int progress = 0,
|
||||||
|
int max = 0,
|
||||||
|
bool indeterminate = false,
|
||||||
|
bool isDetail = false,
|
||||||
|
bool onlyIfFG = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!Platform.isAndroid) {
|
if (!Platform.isAndroid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (_isBackgroundInitialized) {
|
if (_isBackgroundInitialized) {
|
||||||
return await _backgroundChannel
|
return _backgroundChannel.invokeMethod<bool>(
|
||||||
.invokeMethod('updateNotification', [title, content]);
|
'updateNotification',
|
||||||
|
[title, content, progress, max, indeterminate, isDetail, onlyIfFG],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint("[_updateNotification] failed to communicate with plugin");
|
debugPrint("[_updateNotification] failed to communicate with plugin");
|
||||||
}
|
}
|
||||||
return Future.value(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows a new priority notification
|
/// Shows a new priority notification
|
||||||
@@ -274,6 +285,7 @@ class BackgroundService {
|
|||||||
case "onAssetsChanged":
|
case "onAssetsChanged":
|
||||||
final Future<bool> translationsLoaded = loadTranslations();
|
final Future<bool> translationsLoaded = loadTranslations();
|
||||||
try {
|
try {
|
||||||
|
_clearErrorNotifications();
|
||||||
final bool hasAccess = await acquireLock();
|
final bool hasAccess = await acquireLock();
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
debugPrint("[_callHandler] could not acquire lock, exiting");
|
debugPrint("[_callHandler] could not acquire lock, exiting");
|
||||||
@@ -313,19 +325,23 @@ class BackgroundService {
|
|||||||
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
||||||
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
||||||
BackupService backupService = BackupService(apiService);
|
BackupService backupService = BackupService(apiService);
|
||||||
|
AppSettingsService settingsService = AppSettingsService();
|
||||||
|
|
||||||
final Box<HiveBackupAlbums> box =
|
final Box<HiveBackupAlbums> box =
|
||||||
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
||||||
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
||||||
if (backupAlbumInfo == null) {
|
if (backupAlbumInfo == null) {
|
||||||
_clearErrorNotifications();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await PhotoManager.setIgnorePermissionCheck(true);
|
await PhotoManager.setIgnorePermissionCheck(true);
|
||||||
|
|
||||||
do {
|
do {
|
||||||
final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
|
final bool backupOk = await _runBackup(
|
||||||
|
backupService,
|
||||||
|
settingsService,
|
||||||
|
backupAlbumInfo,
|
||||||
|
);
|
||||||
if (backupOk) {
|
if (backupOk) {
|
||||||
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
||||||
await box.put(
|
await box.put(
|
||||||
@@ -346,9 +362,14 @@ class BackgroundService {
|
|||||||
|
|
||||||
Future<bool> _runBackup(
|
Future<bool> _runBackup(
|
||||||
BackupService backupService,
|
BackupService backupService,
|
||||||
|
AppSettingsService settingsService,
|
||||||
HiveBackupAlbums backupAlbumInfo,
|
HiveBackupAlbums backupAlbumInfo,
|
||||||
) async {
|
) async {
|
||||||
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
|
_errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
|
||||||
|
final bool notifyTotalProgress = settingsService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
|
||||||
|
final bool notifySingleProgress = settingsService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
|
||||||
|
|
||||||
if (_canceledBySystem) {
|
if (_canceledBySystem) {
|
||||||
return false;
|
return false;
|
||||||
@@ -372,22 +393,29 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toUpload.isEmpty) {
|
if (toUpload.isEmpty) {
|
||||||
_clearErrorNotifications();
|
|
||||||
return true;
|
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();
|
_cancellationToken = CancellationToken();
|
||||||
final bool ok = await backupService.backupAsset(
|
final bool ok = await backupService.backupAsset(
|
||||||
toUpload,
|
toUpload,
|
||||||
_cancellationToken!,
|
_cancellationToken!,
|
||||||
_onAssetUploaded,
|
notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {},
|
||||||
_onProgress,
|
notifySingleProgress ? _onProgress : (sent, total) {},
|
||||||
_onSetCurrentBackupAsset,
|
notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
|
||||||
_onBackupError,
|
_onBackupError,
|
||||||
);
|
);
|
||||||
if (ok) {
|
if (!ok && !_cancellationToken!.isCancelled) {
|
||||||
_clearErrorNotifications();
|
|
||||||
} else {
|
|
||||||
_showErrorNotification(
|
_showErrorNotification(
|
||||||
title: "backup_background_service_error_title".tr(),
|
title: "backup_background_service_error_title".tr(),
|
||||||
content: "backup_background_service_backup_failed_message".tr(),
|
content: "backup_background_service_backup_failed_message".tr(),
|
||||||
@@ -396,16 +424,43 @@ class BackgroundService {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
String _formatAssetBackupProgress() {
|
||||||
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
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) {
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
||||||
_showErrorNotification(
|
_showErrorNotification(
|
||||||
title: "Upload failed",
|
title: "backup_background_service_upload_failure_notification"
|
||||||
content: "backup_background_service_upload_failure_notification"
|
|
||||||
.tr(args: [errorAssetInfo.fileName]),
|
.tr(args: [errorAssetInfo.fileName]),
|
||||||
individualTag: errorAssetInfo.id,
|
individualTag: errorAssetInfo.id,
|
||||||
);
|
);
|
||||||
@@ -413,14 +468,17 @@ class BackgroundService {
|
|||||||
|
|
||||||
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
||||||
_updateNotification(
|
_updateNotification(
|
||||||
title: "backup_background_service_in_progress_notification".tr(),
|
title: "backup_background_service_current_upload_notification"
|
||||||
content: "backup_background_service_current_upload_notification"
|
|
||||||
.tr(args: [currentUploadAsset.fileName]),
|
.tr(args: [currentUploadAsset.fileName]),
|
||||||
|
content: "",
|
||||||
|
isDetail: true,
|
||||||
|
progress: 0,
|
||||||
|
max: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isErrorGracePeriodExceeded() {
|
bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) {
|
||||||
final int value = AppSettingsService()
|
final int value = appSettingsService
|
||||||
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
|
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
|
||||||
if (value == 0) {
|
if (value == 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -445,6 +503,26 @@ class BackgroundService {
|
|||||||
assert(false, "Invalid value");
|
assert(false, "Invalid value");
|
||||||
return true;
|
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
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
|||||||
DateTime.parse(
|
DateTime.parse(
|
||||||
backupState.currentUploadAsset.createdAt
|
backupState.currentUploadAsset.createdAt
|
||||||
.toString(),
|
.toString(),
|
||||||
),
|
).toLocal(),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
|||||||
DateFormat.yMMMMd('en_US').format(
|
DateFormat.yMMMMd('en_US').format(
|
||||||
DateTime.parse(
|
DateTime.parse(
|
||||||
errorAsset.createdAt.toString(),
|
errorAsset.createdAt.toString(),
|
||||||
),
|
).toLocal(),
|
||||||
),
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
enum RenderAssetGridElementType {
|
||||||
|
assetRow,
|
||||||
|
dayTitle,
|
||||||
|
monthTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderAssetGridRow {
|
||||||
|
final List<AssetResponseDto> assets;
|
||||||
|
|
||||||
|
RenderAssetGridRow(this.assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
class RenderAssetGridElement {
|
||||||
|
final RenderAssetGridElementType type;
|
||||||
|
final RenderAssetGridRow? assetRow;
|
||||||
|
final String? title;
|
||||||
|
final DateTime date;
|
||||||
|
final List<AssetResponseDto>? relatedAssetList;
|
||||||
|
|
||||||
|
RenderAssetGridElement(
|
||||||
|
this.type, {
|
||||||
|
this.assetRow,
|
||||||
|
this.title,
|
||||||
|
required this.date,
|
||||||
|
this.relatedAssetList,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final renderListProvider = StateProvider((ref) {
|
||||||
|
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
|
||||||
|
var settings = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
|
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
|
||||||
|
|
||||||
|
List<RenderAssetGridElement> elements = [];
|
||||||
|
DateTime? lastDate;
|
||||||
|
|
||||||
|
assetGroups.forEach((groupName, assets) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(groupName);
|
||||||
|
|
||||||
|
if (lastDate == null || lastDate!.month != date.month) {
|
||||||
|
elements.add(
|
||||||
|
RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.monthTitle,
|
||||||
|
title: groupName,
|
||||||
|
date: date,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add group title
|
||||||
|
elements.add(
|
||||||
|
RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.dayTitle,
|
||||||
|
title: groupName,
|
||||||
|
date: date,
|
||||||
|
relatedAssetList: assets,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add rows
|
||||||
|
int cursor = 0;
|
||||||
|
while (cursor < assets.length) {
|
||||||
|
int rowElements = min(assets.length - cursor, assetsPerRow);
|
||||||
|
|
||||||
|
final rowElement = RenderAssetGridElement(
|
||||||
|
RenderAssetGridElementType.assetRow,
|
||||||
|
date: date,
|
||||||
|
assetRow: RenderAssetGridRow(
|
||||||
|
assets.sublist(cursor, cursor + rowElements),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
elements.add(rowElement);
|
||||||
|
cursor += rowElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDate = date;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
});
|
||||||
107
mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart
Normal file
107
mobile/lib/modules/home/ui/asset_list_v2/daily_title_text.dart
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class DailyTitleText extends ConsumerWidget {
|
||||||
|
const DailyTitleText({
|
||||||
|
Key? key,
|
||||||
|
required this.isoDate,
|
||||||
|
required this.assetGroup,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final String isoDate;
|
||||||
|
final List<AssetResponseDto> assetGroup;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
var currentYear = DateTime.now().year;
|
||||||
|
var groupYear = DateTime.parse(isoDate).year;
|
||||||
|
var formatDateTemplate = currentYear == groupYear
|
||||||
|
? "daily_title_text_date".tr()
|
||||||
|
: "daily_title_text_date_year".tr();
|
||||||
|
var dateText =
|
||||||
|
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
||||||
|
var isMultiSelectEnable =
|
||||||
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
|
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
||||||
|
var selectedItems = ref.watch(homePageStateProvider).selectedItems;
|
||||||
|
|
||||||
|
void _handleTitleIconClick() {
|
||||||
|
if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedDateGroup.length == 1 &&
|
||||||
|
selectedItems.length <= assetGroup.length) {
|
||||||
|
// Multi select is active - click again on the icon while it is the only active group -> disable multi select
|
||||||
|
ref.watch(homePageStateProvider.notifier).disableMultiSelect();
|
||||||
|
} else if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedItems.length != assetGroup.length) {
|
||||||
|
// Multi select is active - click again on the icon while it is not the only active group -> remove that group from selected group/items
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeSelectedDateGroup(dateText);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeMultipleSelectedItem(assetGroup);
|
||||||
|
} else if (isMultiSelectEnable &&
|
||||||
|
selectedDateGroup.contains(dateText) &&
|
||||||
|
selectedDateGroup.length > 1) {
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeSelectedDateGroup(dateText);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.removeMultipleSelectedItem(assetGroup);
|
||||||
|
} else if (isMultiSelectEnable && !selectedDateGroup.contains(dateText)) {
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addSelectedDateGroup(dateText);
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addMultipleSelectedItems(assetGroup);
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.enableMultiSelect(assetGroup.toSet());
|
||||||
|
ref
|
||||||
|
.watch(homePageStateProvider.notifier)
|
||||||
|
.addSelectedDateGroup(dateText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: 29.0,
|
||||||
|
bottom: 29.0,
|
||||||
|
left: 12.0,
|
||||||
|
right: 12.0,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
dateText,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: _handleTitleIconClick,
|
||||||
|
child: isMultiSelectEnable && selectedDateGroup.contains(dateText)
|
||||||
|
? Icon(
|
||||||
|
Icons.check_circle_rounded,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
)
|
||||||
|
: const Icon(
|
||||||
|
Icons.check_circle_outline_rounded,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
|
/// Build the Scroll Thumb and label using the current configuration
|
||||||
|
typedef ScrollThumbBuilder = Widget Function(
|
||||||
|
Color backgroundColor,
|
||||||
|
Animation<double> thumbAnimation,
|
||||||
|
Animation<double> labelAnimation,
|
||||||
|
double height, {
|
||||||
|
Text? labelText,
|
||||||
|
BoxConstraints? labelConstraints,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Build a Text widget using the current scroll offset
|
||||||
|
typedef LabelTextBuilder = Text Function(int item);
|
||||||
|
|
||||||
|
/// A widget that will display a BoxScrollView with a ScrollThumb that can be dragged
|
||||||
|
/// for quick navigation of the BoxScrollView.
|
||||||
|
class DraggableScrollbar extends StatefulWidget {
|
||||||
|
/// The view that will be scrolled with the scroll thumb
|
||||||
|
final ScrollablePositionedList child;
|
||||||
|
|
||||||
|
final ItemPositionsListener itemPositionsListener;
|
||||||
|
|
||||||
|
/// A function that builds a thumb using the current configuration
|
||||||
|
final ScrollThumbBuilder scrollThumbBuilder;
|
||||||
|
|
||||||
|
/// The height of the scroll thumb
|
||||||
|
final double heightScrollThumb;
|
||||||
|
|
||||||
|
/// The background color of the label and thumb
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// The amount of padding that should surround the thumb
|
||||||
|
final EdgeInsetsGeometry? padding;
|
||||||
|
|
||||||
|
/// Determines how quickly the scrollbar will animate in and out
|
||||||
|
final Duration scrollbarAnimationDuration;
|
||||||
|
|
||||||
|
/// How long should the thumb be visible before fading out
|
||||||
|
final Duration scrollbarTimeToFade;
|
||||||
|
|
||||||
|
/// Build a Text widget from the current offset in the BoxScrollView
|
||||||
|
final LabelTextBuilder? labelTextBuilder;
|
||||||
|
|
||||||
|
/// Determines box constraints for Container displaying label
|
||||||
|
final BoxConstraints? labelConstraints;
|
||||||
|
|
||||||
|
/// The ScrollController for the BoxScrollView
|
||||||
|
final ItemScrollController controller;
|
||||||
|
|
||||||
|
/// Determines scrollThumb displaying. If you draw own ScrollThumb and it is true you just don't need to use animation parameters in [scrollThumbBuilder]
|
||||||
|
final bool alwaysVisibleScrollThumb;
|
||||||
|
|
||||||
|
final Function(bool scrolling) scrollStateListener;
|
||||||
|
|
||||||
|
DraggableScrollbar.semicircle({
|
||||||
|
Key? key,
|
||||||
|
Key? scrollThumbKey,
|
||||||
|
this.alwaysVisibleScrollThumb = false,
|
||||||
|
required this.child,
|
||||||
|
required this.controller,
|
||||||
|
required this.itemPositionsListener,
|
||||||
|
required this.scrollStateListener,
|
||||||
|
this.heightScrollThumb = 48.0,
|
||||||
|
this.backgroundColor = Colors.white,
|
||||||
|
this.padding,
|
||||||
|
this.scrollbarAnimationDuration = const Duration(milliseconds: 300),
|
||||||
|
this.scrollbarTimeToFade = const Duration(milliseconds: 600),
|
||||||
|
this.labelTextBuilder,
|
||||||
|
this.labelConstraints,
|
||||||
|
}) : assert(child.scrollDirection == Axis.vertical),
|
||||||
|
scrollThumbBuilder = _thumbSemicircleBuilder(
|
||||||
|
heightScrollThumb * 0.6,
|
||||||
|
scrollThumbKey,
|
||||||
|
alwaysVisibleScrollThumb,
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
DraggableScrollbarState createState() => DraggableScrollbarState();
|
||||||
|
|
||||||
|
static buildScrollThumbAndLabel({
|
||||||
|
required Widget scrollThumb,
|
||||||
|
required Color backgroundColor,
|
||||||
|
required Animation<double>? thumbAnimation,
|
||||||
|
required Animation<double>? labelAnimation,
|
||||||
|
required Text? labelText,
|
||||||
|
required BoxConstraints? labelConstraints,
|
||||||
|
required bool alwaysVisibleScrollThumb,
|
||||||
|
}) {
|
||||||
|
var scrollThumbAndLabel = labelText == null
|
||||||
|
? scrollThumb
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
ScrollLabel(
|
||||||
|
animation: labelAnimation,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
constraints: labelConstraints,
|
||||||
|
child: labelText,
|
||||||
|
),
|
||||||
|
scrollThumb,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (alwaysVisibleScrollThumb) {
|
||||||
|
return scrollThumbAndLabel;
|
||||||
|
}
|
||||||
|
return SlideFadeTransition(
|
||||||
|
animation: thumbAnimation!,
|
||||||
|
child: scrollThumbAndLabel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ScrollThumbBuilder _thumbSemicircleBuilder(
|
||||||
|
double width,
|
||||||
|
Key? scrollThumbKey,
|
||||||
|
bool alwaysVisibleScrollThumb,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
Color backgroundColor,
|
||||||
|
Animation<double> thumbAnimation,
|
||||||
|
Animation<double> labelAnimation,
|
||||||
|
double height, {
|
||||||
|
Text? labelText,
|
||||||
|
BoxConstraints? labelConstraints,
|
||||||
|
}) {
|
||||||
|
final scrollThumb = CustomPaint(
|
||||||
|
key: scrollThumbKey,
|
||||||
|
foregroundPainter: ArrowCustomPainter(Colors.white),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(height),
|
||||||
|
bottomLeft: Radius.circular(height),
|
||||||
|
topRight: const Radius.circular(4.0),
|
||||||
|
bottomRight: const Radius.circular(4.0),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.tight(Size(width, height)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return buildScrollThumbAndLabel(
|
||||||
|
scrollThumb: scrollThumb,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
thumbAnimation: thumbAnimation,
|
||||||
|
labelAnimation: labelAnimation,
|
||||||
|
labelText: labelText,
|
||||||
|
labelConstraints: labelConstraints,
|
||||||
|
alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScrollLabel extends StatelessWidget {
|
||||||
|
final Animation<double>? animation;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final Text child;
|
||||||
|
|
||||||
|
final BoxConstraints? constraints;
|
||||||
|
static const BoxConstraints _defaultConstraints =
|
||||||
|
BoxConstraints.tightFor(width: 72.0, height: 28.0);
|
||||||
|
|
||||||
|
const ScrollLabel({
|
||||||
|
Key? key,
|
||||||
|
required this.child,
|
||||||
|
required this.animation,
|
||||||
|
required this.backgroundColor,
|
||||||
|
this.constraints = _defaultConstraints,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation!,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(right: 12.0),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints ?? _defaultConstraints,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraggableScrollbarState extends State<DraggableScrollbar>
|
||||||
|
with TickerProviderStateMixin {
|
||||||
|
late double _barOffset;
|
||||||
|
late bool _isDragInProcess;
|
||||||
|
late int _currentItem;
|
||||||
|
|
||||||
|
late AnimationController _thumbAnimationController;
|
||||||
|
late Animation<double> _thumbAnimation;
|
||||||
|
late AnimationController _labelAnimationController;
|
||||||
|
late Animation<double> _labelAnimation;
|
||||||
|
Timer? _fadeoutTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_barOffset = 0.0;
|
||||||
|
_isDragInProcess = false;
|
||||||
|
_currentItem = 0;
|
||||||
|
|
||||||
|
_thumbAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: widget.scrollbarAnimationDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
_thumbAnimation = CurvedAnimation(
|
||||||
|
parent: _thumbAnimationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
_labelAnimationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: widget.scrollbarAnimationDuration,
|
||||||
|
);
|
||||||
|
|
||||||
|
_labelAnimation = CurvedAnimation(
|
||||||
|
parent: _labelAnimationController,
|
||||||
|
curve: Curves.fastOutSlowIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_thumbAnimationController.dispose();
|
||||||
|
_labelAnimationController.dispose();
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
double get barMaxScrollExtent =>
|
||||||
|
(context.size?.height ?? 0) - widget.heightScrollThumb;
|
||||||
|
|
||||||
|
double get barMinScrollExtent => 0;
|
||||||
|
|
||||||
|
int get maxItemCount => widget.child.itemCount;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Text? labelText;
|
||||||
|
if (widget.labelTextBuilder != null && _isDragInProcess) {
|
||||||
|
labelText = widget.labelTextBuilder!(_currentItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
//print("LayoutBuilder constraints=$constraints");
|
||||||
|
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (ScrollNotification notification) {
|
||||||
|
changePosition(notification);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
RepaintBoundary(
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
RepaintBoundary(
|
||||||
|
child: GestureDetector(
|
||||||
|
onVerticalDragStart: _onVerticalDragStart,
|
||||||
|
onVerticalDragUpdate: _onVerticalDragUpdate,
|
||||||
|
onVerticalDragEnd: _onVerticalDragEnd,
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
margin: EdgeInsets.only(top: _barOffset),
|
||||||
|
padding: widget.padding,
|
||||||
|
child: widget.scrollThumbBuilder(
|
||||||
|
widget.backgroundColor,
|
||||||
|
_thumbAnimation,
|
||||||
|
_labelAnimation,
|
||||||
|
widget.heightScrollThumb,
|
||||||
|
labelText: labelText,
|
||||||
|
labelConstraints: widget.labelConstraints,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll bar has received notification that it's view was scrolled
|
||||||
|
// so it should also changes his position
|
||||||
|
// but only if it isn't dragged
|
||||||
|
changePosition(ScrollNotification notification) {
|
||||||
|
if (_isDragInProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
int firstItemIndex =
|
||||||
|
widget.itemPositionsListener.itemPositions.value.first.index;
|
||||||
|
|
||||||
|
if (notification is ScrollUpdateNotification) {
|
||||||
|
_barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent;
|
||||||
|
|
||||||
|
if (_barOffset < barMinScrollExtent) {
|
||||||
|
_barOffset = barMinScrollExtent;
|
||||||
|
}
|
||||||
|
if (_barOffset > barMaxScrollExtent) {
|
||||||
|
_barOffset = barMaxScrollExtent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification is ScrollUpdateNotification ||
|
||||||
|
notification is OverscrollNotification) {
|
||||||
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
|
_thumbAnimationController.forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemPos < maxItemCount) {
|
||||||
|
_currentItem = itemPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
_fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
|
||||||
|
_thumbAnimationController.reverse();
|
||||||
|
_labelAnimationController.reverse();
|
||||||
|
_fadeoutTimer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalDragStart(DragStartDetails details) {
|
||||||
|
setState(() {
|
||||||
|
_isDragInProcess = true;
|
||||||
|
_labelAnimationController.forward();
|
||||||
|
_fadeoutTimer?.cancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.scrollStateListener(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get itemPos {
|
||||||
|
int numberOfItems = widget.child.itemCount;
|
||||||
|
return ((_barOffset / barMaxScrollExtent) * numberOfItems).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _jumpToBarPos() {
|
||||||
|
if (itemPos > maxItemCount - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentItem = itemPos;
|
||||||
|
|
||||||
|
widget.controller.jumpTo(
|
||||||
|
index: itemPos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer? dragHaltTimer;
|
||||||
|
int lastTimerPos = 0;
|
||||||
|
|
||||||
|
void _onVerticalDragUpdate(DragUpdateDetails details) {
|
||||||
|
setState(() {
|
||||||
|
if (_thumbAnimationController.status != AnimationStatus.forward) {
|
||||||
|
_thumbAnimationController.forward();
|
||||||
|
}
|
||||||
|
if (_isDragInProcess) {
|
||||||
|
_barOffset += details.delta.dy;
|
||||||
|
|
||||||
|
if (_barOffset < barMinScrollExtent) {
|
||||||
|
_barOffset = barMinScrollExtent;
|
||||||
|
}
|
||||||
|
if (_barOffset > barMaxScrollExtent) {
|
||||||
|
_barOffset = barMaxScrollExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemPos != lastTimerPos) {
|
||||||
|
lastTimerPos = itemPos;
|
||||||
|
dragHaltTimer?.cancel();
|
||||||
|
widget.scrollStateListener(true);
|
||||||
|
|
||||||
|
dragHaltTimer = Timer(
|
||||||
|
const Duration(milliseconds: 200),
|
||||||
|
() {
|
||||||
|
widget.scrollStateListener(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_jumpToBarPos();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onVerticalDragEnd(DragEndDetails details) {
|
||||||
|
_fadeoutTimer = Timer(widget.scrollbarTimeToFade, () {
|
||||||
|
_thumbAnimationController.reverse();
|
||||||
|
_labelAnimationController.reverse();
|
||||||
|
_fadeoutTimer = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_jumpToBarPos();
|
||||||
|
_isDragInProcess = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.scrollStateListener(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws 2 triangles like arrow up and arrow down
|
||||||
|
class ArrowCustomPainter extends CustomPainter {
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
ArrowCustomPainter(this.color);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint = Paint()..color = color;
|
||||||
|
const width = 12.0;
|
||||||
|
const height = 8.0;
|
||||||
|
final baseX = size.width / 2;
|
||||||
|
final baseY = size.height / 2;
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
_trianglePath(Offset(baseX, baseY - 2.0), width, height, true),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
canvas.drawPath(
|
||||||
|
_trianglePath(Offset(baseX, baseY + 2.0), width, height, false),
|
||||||
|
paint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Path _trianglePath(Offset o, double width, double height, bool isUp) {
|
||||||
|
return Path()
|
||||||
|
..moveTo(o.dx, o.dy)
|
||||||
|
..lineTo(o.dx + width, o.dy)
|
||||||
|
..lineTo(o.dx + (width / 2), isUp ? o.dy - height : o.dy + height)
|
||||||
|
..close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///This cut 2 lines in arrow shape
|
||||||
|
class ArrowClipper extends CustomClipper<Path> {
|
||||||
|
@override
|
||||||
|
Path getClip(Size size) {
|
||||||
|
Path path = Path();
|
||||||
|
path.lineTo(0.0, size.height);
|
||||||
|
path.lineTo(size.width, size.height);
|
||||||
|
path.lineTo(size.width, 0.0);
|
||||||
|
path.lineTo(0.0, 0.0);
|
||||||
|
path.close();
|
||||||
|
|
||||||
|
double arrowWidth = 8.0;
|
||||||
|
double startPointX = (size.width - arrowWidth) / 2;
|
||||||
|
double startPointY = size.height / 2 - arrowWidth / 2;
|
||||||
|
path.moveTo(startPointX, startPointY);
|
||||||
|
path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
|
||||||
|
path.lineTo(startPointX + arrowWidth, startPointY);
|
||||||
|
path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
|
||||||
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY - arrowWidth / 2 + 1.0,
|
||||||
|
);
|
||||||
|
path.lineTo(startPointX, startPointY + 1.0);
|
||||||
|
path.close();
|
||||||
|
|
||||||
|
startPointY = size.height / 2 + arrowWidth / 2;
|
||||||
|
path.moveTo(startPointX + arrowWidth, startPointY);
|
||||||
|
path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
|
||||||
|
path.lineTo(startPointX, startPointY);
|
||||||
|
path.lineTo(startPointX, startPointY - 1.0);
|
||||||
|
path.lineTo(
|
||||||
|
startPointX + arrowWidth / 2,
|
||||||
|
startPointY + arrowWidth / 2 - 1.0,
|
||||||
|
);
|
||||||
|
path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
|
||||||
|
path.close();
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SlideFadeTransition extends StatelessWidget {
|
||||||
|
final Animation<double> animation;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const SlideFadeTransition({
|
||||||
|
Key? key,
|
||||||
|
required this.animation,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: animation,
|
||||||
|
builder: (context, child) =>
|
||||||
|
animation.value == 0.0 ? const SizedBox() : child!,
|
||||||
|
child: SlideTransition(
|
||||||
|
position: Tween(
|
||||||
|
begin: const Offset(0.3, 0.0),
|
||||||
|
end: const Offset(0.0, 0.0),
|
||||||
|
).animate(animation),
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
167
mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart
Normal file
167
mobile/lib/modules/home/ui/asset_list_v2/immich_asset_grid.dart
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
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:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_list_v2/daily_title_text.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_list_v2/draggable_scrollbar_custom.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||||
|
|
||||||
|
import '../thumbnail_image.dart';
|
||||||
|
|
||||||
|
class ImmichAssetGrid extends HookConsumerWidget {
|
||||||
|
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||||
|
final ItemPositionsListener _itemPositionsListener =
|
||||||
|
ItemPositionsListener.create();
|
||||||
|
|
||||||
|
final List<RenderAssetGridElement> renderList;
|
||||||
|
final int assetsPerRow;
|
||||||
|
final double margin;
|
||||||
|
final bool showStorageIndicator;
|
||||||
|
|
||||||
|
ImmichAssetGrid({
|
||||||
|
super.key,
|
||||||
|
required this.renderList,
|
||||||
|
required this.assetsPerRow,
|
||||||
|
required this.showStorageIndicator,
|
||||||
|
this.margin = 5.0,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<AssetResponseDto> get _assets {
|
||||||
|
return renderList
|
||||||
|
.map((e) {
|
||||||
|
if (e.type == RenderAssetGridElementType.assetRow) {
|
||||||
|
return e.assetRow!.assets;
|
||||||
|
} else {
|
||||||
|
return List<AssetResponseDto>.empty();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.flattened
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _getItemSize(BuildContext context) {
|
||||||
|
return MediaQuery.of(context).size.width / assetsPerRow -
|
||||||
|
margin * (assetsPerRow - 1) / assetsPerRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildThumbnailOrPlaceholder(
|
||||||
|
AssetResponseDto asset, bool placeholder) {
|
||||||
|
if (placeholder) {
|
||||||
|
return const DecoratedBox(
|
||||||
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ThumbnailImage(
|
||||||
|
asset: asset,
|
||||||
|
assetList: _assets,
|
||||||
|
showStorageIndicator: showStorageIndicator,
|
||||||
|
useGrayBoxPlaceholder: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAssetRow(
|
||||||
|
BuildContext context, RenderAssetGridRow row, bool scrolling) {
|
||||||
|
double size = _getItemSize(context);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
key: Key("asset-row-${row.assets.first.id}"),
|
||||||
|
children: row.assets.map((AssetResponseDto asset) {
|
||||||
|
bool last = asset == row.assets.last;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
key: Key("asset-${asset.id}"),
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
margin: EdgeInsets.only(top: margin, right: last ? 0.0 : margin),
|
||||||
|
child: _buildThumbnailOrPlaceholder(asset, scrolling),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTitle(
|
||||||
|
BuildContext context, String title, List<AssetResponseDto> assets) {
|
||||||
|
return DailyTitleText(
|
||||||
|
isoDate: title,
|
||||||
|
assetGroup: assets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMonthTitle(BuildContext context, String title) {
|
||||||
|
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
||||||
|
.format(DateTime.parse(title));
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
key: Key("month-$title"),
|
||||||
|
padding: const EdgeInsets.only(left: 12.0, top: 32),
|
||||||
|
child: Text(
|
||||||
|
monthTitleText,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).textTheme.headline1?.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _itemBuilder(BuildContext c, int position, bool scrolling) {
|
||||||
|
final item = renderList[position];
|
||||||
|
|
||||||
|
if (item.type == RenderAssetGridElementType.dayTitle) {
|
||||||
|
return _buildTitle(c, item.title!, item.relatedAssetList!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.monthTitle) {
|
||||||
|
return _buildMonthTitle(c, item.title!);
|
||||||
|
} else if (item.type == RenderAssetGridElementType.assetRow) {
|
||||||
|
return _buildAssetRow(c, item.assetRow!, scrolling);
|
||||||
|
}
|
||||||
|
|
||||||
|
return const Text("Invalid widget type!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Text _labelBuilder(int pos) {
|
||||||
|
final date = renderList[pos].date;
|
||||||
|
return Text(DateFormat.yMMMd().format(date),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final scrolling = useState(false);
|
||||||
|
|
||||||
|
void dragScrolling(bool active) {
|
||||||
|
scrolling.value = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget itemBuilder(BuildContext c, int position) {
|
||||||
|
return _itemBuilder(c, position, scrolling.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DraggableScrollbar.semicircle(
|
||||||
|
scrollStateListener: dragScrolling,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
controller: _itemScrollController,
|
||||||
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
|
labelTextBuilder: _labelBuilder,
|
||||||
|
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||||
|
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||||
|
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||||
|
child: ScrollablePositionedList.builder(
|
||||||
|
itemBuilder: itemBuilder,
|
||||||
|
itemPositionsListener: _itemPositionsListener,
|
||||||
|
itemScrollController: _itemScrollController,
|
||||||
|
itemCount: renderList.length,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,8 +21,8 @@ class DailyTitleText extends ConsumerWidget {
|
|||||||
var formatDateTemplate = currentYear == groupYear
|
var formatDateTemplate = currentYear == groupYear
|
||||||
? "daily_title_text_date".tr()
|
? "daily_title_text_date".tr()
|
||||||
: "daily_title_text_date_year".tr();
|
: "daily_title_text_date_year".tr();
|
||||||
var dateText =
|
var dateText = DateFormat(formatDateTemplate)
|
||||||
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
|
.format(DateTime.parse(isoDate).toLocal());
|
||||||
var isMultiSelectEnable =
|
var isMultiSelectEnable =
|
||||||
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
ref.watch(homePageStateProvider).isMultiSelectEnable;
|
||||||
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
var selectedDateGroup = ref.watch(homePageStateProvider).selectedDateGroup;
|
||||||
|
|||||||
@@ -33,34 +33,10 @@ class ImageGrid extends ConsumerWidget {
|
|||||||
var assetType = assetGroup[index].type;
|
var assetType = assetGroup[index].type;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
child: Stack(
|
child: ThumbnailImage(
|
||||||
children: [
|
asset: assetGroup[index],
|
||||||
ThumbnailImage(
|
assetList: sortedAssetGroup,
|
||||||
asset: assetGroup[index],
|
showStorageIndicator: showStorageIndicator,
|
||||||
assetList: sortedAssetGroup,
|
|
||||||
showStorageIndicator: showStorageIndicator,
|
|
||||||
),
|
|
||||||
if (assetType != AssetTypeEnum.IMAGE)
|
|
||||||
Positioned(
|
|
||||||
top: 5,
|
|
||||||
right: 5,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
assetGroup[index].duration.toString().substring(0, 7),
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(
|
|
||||||
Icons.play_circle_outline_rounded,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class MonthlyTitleText extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
|
||||||
.format(DateTime.parse(isoDate));
|
.format(DateTime.parse(isoDate).toLocal());
|
||||||
|
|
||||||
return SliverToBoxAdapter(
|
return SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
final AssetResponseDto asset;
|
final AssetResponseDto asset;
|
||||||
final List<AssetResponseDto> assetList;
|
final List<AssetResponseDto> assetList;
|
||||||
final bool showStorageIndicator;
|
final bool showStorageIndicator;
|
||||||
|
final bool useGrayBoxPlaceholder;
|
||||||
|
|
||||||
const ThumbnailImage({
|
const ThumbnailImage({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.assetList,
|
required this.assetList,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator = true,
|
||||||
|
this.useGrayBoxPlaceholder = false,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -102,13 +104,19 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
||||||
},
|
},
|
||||||
fadeInDuration: const Duration(milliseconds: 250),
|
fadeInDuration: const Duration(milliseconds: 250),
|
||||||
progressIndicatorBuilder: (context, url, downloadProgress) =>
|
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||||
Transform.scale(
|
if (useGrayBoxPlaceholder) {
|
||||||
scale: 0.2,
|
return const DecoratedBox(
|
||||||
child: CircularProgressIndicator(
|
decoration: BoxDecoration(color: Colors.grey),
|
||||||
value: downloadProgress.progress,
|
);
|
||||||
),
|
}
|
||||||
),
|
return Transform.scale(
|
||||||
|
scale: 0.2,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
value: downloadProgress.progress,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
errorWidget: (context, url, error) {
|
errorWidget: (context, url, error) {
|
||||||
debugPrint("Error getting thumbnail $url = $error");
|
debugPrint("Error getting thumbnail $url = $error");
|
||||||
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
CachedNetworkImage.evictFromCache(thumbnailRequestUrl);
|
||||||
@@ -139,7 +147,27 @@ class ThumbnailImage extends HookConsumerWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
if (asset.type != AssetTypeEnum.IMAGE)
|
||||||
|
Positioned(
|
||||||
|
top: 5,
|
||||||
|
right: 5,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
asset.duration.toString().substring(0, 7),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
Icons.play_circle_outline_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/providers/home_page_render_list_provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/daily_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
|
import 'package:immich_mobile/modules/home/ui/disable_multi_select_button.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
import 'package:immich_mobile/modules/home/ui/image_grid.dart';
|
||||||
|
import 'package:immich_mobile/modules/home/ui/asset_list_v2/immich_asset_grid.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
|
||||||
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.dart';
|
||||||
@@ -25,6 +27,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
|
var renderList = ref.watch(renderListProvider);
|
||||||
|
|
||||||
ScrollController scrollController = useScrollController();
|
ScrollController scrollController = useScrollController();
|
||||||
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
|
||||||
List<Widget> imageGridGroup = [];
|
List<Widget> imageGridGroup = [];
|
||||||
@@ -120,6 +124,31 @@ class HomePage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_buildAssetGrid() {
|
||||||
|
if (appSettingService
|
||||||
|
.getSetting(AppSettingsEnum.useExperimentalAssetGrid)) {
|
||||||
|
return ImmichAssetGrid(
|
||||||
|
renderList: renderList,
|
||||||
|
assetsPerRow:
|
||||||
|
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
|
showStorageIndicator: appSettingService
|
||||||
|
.getSetting(AppSettingsEnum.storageIndicator),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return DraggableScrollbar.semicircle(
|
||||||
|
backgroundColor: Theme.of(context).hintColor,
|
||||||
|
controller: scrollController,
|
||||||
|
heightScrollThumb: 48.0,
|
||||||
|
child: CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
|
slivers: [
|
||||||
|
...imageGridGroup,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: !isMultiSelectEnable,
|
bottom: !isMultiSelectEnable,
|
||||||
top: !isMultiSelectEnable,
|
top: !isMultiSelectEnable,
|
||||||
@@ -132,17 +161,7 @@ class HomePage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
|
padding: const EdgeInsets.only(top: 60.0, bottom: 0.0),
|
||||||
child: DraggableScrollbar.semicircle(
|
child: _buildAssetGrid(),
|
||||||
backgroundColor: Theme.of(context).hintColor,
|
|
||||||
controller: scrollController,
|
|
||||||
heightScrollThumb: 48.0,
|
|
||||||
child: CustomScrollView(
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [
|
|
||||||
...imageGridGroup,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (isMultiSelectEnable) ...[
|
if (isMultiSelectEnable) ...[
|
||||||
_buildSelectedItemCountIndicator(),
|
_buildSelectedItemCountIndicator(),
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ final searchResultGroupByDateTimeProvider = StateProvider((ref) {
|
|||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) =>
|
(element) => DateFormat('y-MM-dd')
|
||||||
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
|
.format(DateTime.parse(element.createdAt).toLocal()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
_buildThings() {
|
_buildThings() {
|
||||||
return curatedObjects.when(
|
return curatedObjects.when(
|
||||||
loading: () => const SizedBox(
|
loading: () => SizedBox(
|
||||||
height: 200,
|
height: imageSize,
|
||||||
child: Center(child: ImmichLoadingIndicator()),
|
child: const Center(child: ImmichLoadingIndicator()),
|
||||||
),
|
),
|
||||||
error: (err, stack) => Text('Error: $err'),
|
error: (err, stack) => Text('Error: $err'),
|
||||||
data: (objects) {
|
data: (objects) {
|
||||||
@@ -133,8 +133,7 @@ class SearchPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: SizedBox(
|
: SizedBox(
|
||||||
// height: imageSize,
|
height: imageSize,
|
||||||
width: imageSize,
|
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(left: 16),
|
padding: const EdgeInsets.only(left: 16),
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ enum AppSettingsEnum<T> {
|
|||||||
themeMode<String>("themeMode", "system"), // "light","dark","system"
|
themeMode<String>("themeMode", "system"), // "light","dark","system"
|
||||||
tilesPerRow<int>("tilesPerRow", 4),
|
tilesPerRow<int>("tilesPerRow", 4),
|
||||||
uploadErrorNotificationGracePeriod<int>(
|
uploadErrorNotificationGracePeriod<int>(
|
||||||
"uploadErrorNotificationGracePeriod", 2),
|
"uploadErrorNotificationGracePeriod",
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
backgroundBackupTotalProgress<bool>("backgroundBackupTotalProgress", true),
|
||||||
|
backgroundBackupSingleProgress<bool>("backgroundBackupSingleProgress", false),
|
||||||
storageIndicator<bool>("storageIndicator", true),
|
storageIndicator<bool>("storageIndicator", true),
|
||||||
thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
|
thumbnailCacheSize<int>("thumbnailCacheSize", 10000),
|
||||||
imageCacheSize<int>("imageCacheSize", 350),
|
imageCacheSize<int>("imageCacheSize", 350),
|
||||||
albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200);
|
albumThumbnailCacheSize<int>("albumThumbnailCacheSize", 200),
|
||||||
|
useExperimentalAssetGrid<bool>("useExperimentalAssetGrid", false);
|
||||||
|
|
||||||
const AppSettingsEnum(this.hiveKey, this.defaultValue);
|
const AppSettingsEnum(this.hiveKey, this.defaultValue);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
|
|
||||||
|
class ExperimentalSettings extends HookConsumerWidget {
|
||||||
|
const ExperimentalSettings({
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
|
final useExperimentalAssetGrid = useState(false);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
useExperimentalAssetGrid.value = appSettingService
|
||||||
|
.getSetting(AppSettingsEnum.useExperimentalAssetGrid);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
void changeUseExperimentalAssetGrid(bool status) {
|
||||||
|
useExperimentalAssetGrid.value = status;
|
||||||
|
appSettingService.setSetting(
|
||||||
|
AppSettingsEnum.useExperimentalAssetGrid,
|
||||||
|
status,
|
||||||
|
);
|
||||||
|
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: "settings_require_restart".tr(),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExpansionTile(
|
||||||
|
textColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
'experimental_settings_title',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
subtitle: const Text(
|
||||||
|
'experimental_settings_subtitle',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
title: const Text(
|
||||||
|
"experimental_settings_new_asset_list_title",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
subtitle: const Text(
|
||||||
|
"experimental_settings_new_asset_list_subtitle",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
value: useExperimentalAssetGrid.value,
|
||||||
|
onChanged: changeUseExperimentalAssetGrid,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,12 +15,20 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
|
||||||
final sliderValue = useState(0.0);
|
final sliderValue = useState(0.0);
|
||||||
|
final totalProgressValue =
|
||||||
|
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
|
||||||
|
final singleProgressValue =
|
||||||
|
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
sliderValue.value = appSettingService
|
sliderValue.value = appSettingService
|
||||||
.getSetting<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
|
.getSetting<int>(AppSettingsEnum.uploadErrorNotificationGracePeriod)
|
||||||
.toDouble();
|
.toDouble();
|
||||||
|
totalProgressValue.value = appSettingService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
|
||||||
|
singleProgressValue.value = appSettingService
|
||||||
|
.getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -42,6 +50,22 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
children: [
|
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(
|
ListTile(
|
||||||
isThreeLine: false,
|
isThreeLine: false,
|
||||||
dense: true,
|
dense: true,
|
||||||
@@ -53,7 +77,9 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
value: sliderValue.value,
|
value: sliderValue.value,
|
||||||
onChanged: (double v) => sliderValue.value = v,
|
onChanged: (double v) => sliderValue.value = v,
|
||||||
onChangeEnd: (double v) => appSettingService.setSetting(
|
onChangeEnd: (double v) => appSettingService.setSetting(
|
||||||
AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()),
|
AppSettingsEnum.uploadErrorNotificationGracePeriod,
|
||||||
|
v.toInt(),
|
||||||
|
),
|
||||||
max: 5.0,
|
max: 5.0,
|
||||||
divisions: 5,
|
divisions: 5,
|
||||||
label: formattedValue,
|
label: formattedValue,
|
||||||
@@ -65,6 +91,28 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwitchListTile _buildSwitchListTile(
|
||||||
|
BuildContext context,
|
||||||
|
AppSettingsService appSettingService,
|
||||||
|
ValueNotifier<bool> 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) {
|
String _formatSliderValue(double v) {
|
||||||
if (v == 0.0) {
|
if (v == 0.0) {
|
||||||
return 'setting_notifications_notify_immediately'.tr();
|
return 'setting_notifications_notify_immediately'.tr();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/ui/experimental_settings/experimental_settings.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/image_viewer_quality_setting/image_viewer_quality_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/notification_setting/notification_setting.dart';
|
||||||
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
import 'package:immich_mobile/modules/settings/ui/theme_setting/theme_setting.dart';
|
||||||
@@ -42,6 +43,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
const ThemeSetting(),
|
const ThemeSetting(),
|
||||||
const AssetListSettings(),
|
const AssetListSettings(),
|
||||||
if (Platform.isAndroid) const NotificationSetting(),
|
if (Platform.isAndroid) const NotificationSetting(),
|
||||||
|
const ExperimentalSettings(),
|
||||||
],
|
],
|
||||||
).toList(),
|
).toList(),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ final assetGroupByDateTimeProvider = StateProvider((ref) {
|
|||||||
(a, b) => b.compareTo(a),
|
(a, b) => b.compareTo(a),
|
||||||
);
|
);
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) =>
|
(element) => DateFormat('y-MM-dd')
|
||||||
DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)),
|
.format(DateTime.parse(element.createdAt).toLocal()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ final assetGroupByMonthYearProvider = StateProvider((ref) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return assets.groupListsBy(
|
return assets.groupListsBy(
|
||||||
(element) =>
|
(element) => DateFormat('MMMM, y')
|
||||||
DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)),
|
.format(DateTime.parse(element.createdAt).toLocal()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ImmichToast {
|
|||||||
ToastType toastType = ToastType.info,
|
ToastType toastType = ToastType.info,
|
||||||
ToastGravity gravity = ToastGravity.TOP,
|
ToastGravity gravity = ToastGravity.TOP,
|
||||||
}) {
|
}) {
|
||||||
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
final fToast = FToast();
|
final fToast = FToast();
|
||||||
fToast.init(context);
|
fToast.init(context);
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ class ImmichToast {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
color: Colors.grey[50],
|
color: isDarkTheme ? Colors.grey[900] : Colors.grey[50],
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
width: 1,
|
width: 1,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ doc/AdminSignupResponseDto.md
|
|||||||
doc/AlbumApi.md
|
doc/AlbumApi.md
|
||||||
doc/AlbumCountResponseDto.md
|
doc/AlbumCountResponseDto.md
|
||||||
doc/AlbumResponseDto.md
|
doc/AlbumResponseDto.md
|
||||||
|
doc/AllJobStatusResponseDto.md
|
||||||
doc/AssetApi.md
|
doc/AssetApi.md
|
||||||
doc/AssetCountByTimeBucket.md
|
doc/AssetCountByTimeBucket.md
|
||||||
doc/AssetCountByTimeBucketResponseDto.md
|
doc/AssetCountByTimeBucketResponseDto.md
|
||||||
@@ -33,6 +34,12 @@ doc/DeviceTypeEnum.md
|
|||||||
doc/ExifResponseDto.md
|
doc/ExifResponseDto.md
|
||||||
doc/GetAssetByTimeBucketDto.md
|
doc/GetAssetByTimeBucketDto.md
|
||||||
doc/GetAssetCountByTimeBucketDto.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/LoginCredentialDto.md
|
||||||
doc/LoginResponseDto.md
|
doc/LoginResponseDto.md
|
||||||
doc/LogoutResponseDto.md
|
doc/LogoutResponseDto.md
|
||||||
@@ -59,6 +66,7 @@ lib/api/album_api.dart
|
|||||||
lib/api/asset_api.dart
|
lib/api/asset_api.dart
|
||||||
lib/api/authentication_api.dart
|
lib/api/authentication_api.dart
|
||||||
lib/api/device_info_api.dart
|
lib/api/device_info_api.dart
|
||||||
|
lib/api/job_api.dart
|
||||||
lib/api/server_info_api.dart
|
lib/api/server_info_api.dart
|
||||||
lib/api/user_api.dart
|
lib/api/user_api.dart
|
||||||
lib/api_client.dart
|
lib/api_client.dart
|
||||||
@@ -74,6 +82,7 @@ lib/model/add_users_dto.dart
|
|||||||
lib/model/admin_signup_response_dto.dart
|
lib/model/admin_signup_response_dto.dart
|
||||||
lib/model/album_count_response_dto.dart
|
lib/model/album_count_response_dto.dart
|
||||||
lib/model/album_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.dart
|
||||||
lib/model/asset_count_by_time_bucket_response_dto.dart
|
lib/model/asset_count_by_time_bucket_response_dto.dart
|
||||||
lib/model/asset_count_by_user_id_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/exif_response_dto.dart
|
||||||
lib/model/get_asset_by_time_bucket_dto.dart
|
lib/model/get_asset_by_time_bucket_dto.dart
|
||||||
lib/model/get_asset_count_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_credential_dto.dart
|
||||||
lib/model/login_response_dto.dart
|
lib/model/login_response_dto.dart
|
||||||
lib/model/logout_response_dto.dart
|
lib/model/logout_response_dto.dart
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ Class | Method | HTTP request | Description
|
|||||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||||
*DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info |
|
*DeviceInfoApi* | [**createDeviceInfo**](doc//DeviceInfoApi.md#createdeviceinfo) | **POST** /device-info |
|
||||||
*DeviceInfoApi* | [**updateDeviceInfo**](doc//DeviceInfoApi.md#updatedeviceinfo) | **PATCH** /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* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |
|
||||||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||||
@@ -117,6 +120,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
|
- [AdminSignupResponseDto](doc//AdminSignupResponseDto.md)
|
||||||
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
|
||||||
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
- [AlbumResponseDto](doc//AlbumResponseDto.md)
|
||||||
|
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
|
||||||
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
|
- [AssetCountByTimeBucket](doc//AssetCountByTimeBucket.md)
|
||||||
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
|
- [AssetCountByTimeBucketResponseDto](doc//AssetCountByTimeBucketResponseDto.md)
|
||||||
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
|
- [AssetCountByUserIdResponseDto](doc//AssetCountByUserIdResponseDto.md)
|
||||||
@@ -139,6 +143,11 @@ Class | Method | HTTP request | Description
|
|||||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||||
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
|
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
|
||||||
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.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)
|
- [LoginCredentialDto](doc//LoginCredentialDto.md)
|
||||||
- [LoginResponseDto](doc//LoginResponseDto.md)
|
- [LoginResponseDto](doc//LoginResponseDto.md)
|
||||||
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
- [LogoutResponseDto](doc//LogoutResponseDto.md)
|
||||||
|
|||||||
22
mobile/openapi/doc/AllJobStatusResponseDto.md
Normal file
22
mobile/openapi/doc/AllJobStatusResponseDto.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
15
mobile/openapi/doc/CreateJobDto.md
Normal file
15
mobile/openapi/doc/CreateJobDto.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -8,13 +8,13 @@ import 'package:openapi/api.dart';
|
|||||||
## Properties
|
## Properties
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**id** | **String** | | [optional]
|
**id** | **int** | | [optional]
|
||||||
|
**fileSizeInByte** | **int** | | [optional]
|
||||||
**make** | **String** | | [optional]
|
**make** | **String** | | [optional]
|
||||||
**model** | **String** | | [optional]
|
**model** | **String** | | [optional]
|
||||||
**imageName** | **String** | | [optional]
|
**imageName** | **String** | | [optional]
|
||||||
**exifImageWidth** | **num** | | [optional]
|
**exifImageWidth** | **num** | | [optional]
|
||||||
**exifImageHeight** | **num** | | [optional]
|
**exifImageHeight** | **num** | | [optional]
|
||||||
**fileSizeInByte** | **num** | | [optional]
|
|
||||||
**orientation** | **String** | | [optional]
|
**orientation** | **String** | | [optional]
|
||||||
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
|
**dateTimeOriginal** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]
|
**modifyDate** | [**DateTime**](DateTime.md) | | [optional]
|
||||||
|
|||||||
155
mobile/openapi/doc/JobApi.md
Normal file
155
mobile/openapi/doc/JobApi.md
Normal file
@@ -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<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('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<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('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<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('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)
|
||||||
|
|
||||||
14
mobile/openapi/doc/JobCommand.md
Normal file
14
mobile/openapi/doc/JobCommand.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
15
mobile/openapi/doc/JobCommandDto.md
Normal file
15
mobile/openapi/doc/JobCommandDto.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
19
mobile/openapi/doc/JobCounts.md
Normal file
19
mobile/openapi/doc/JobCounts.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
14
mobile/openapi/doc/JobId.md
Normal file
14
mobile/openapi/doc/JobId.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
16
mobile/openapi/doc/JobStatusResponseDto.md
Normal file
16
mobile/openapi/doc/JobStatusResponseDto.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
14
mobile/openapi/doc/JobType.md
Normal file
14
mobile/openapi/doc/JobType.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ part 'api/album_api.dart';
|
|||||||
part 'api/asset_api.dart';
|
part 'api/asset_api.dart';
|
||||||
part 'api/authentication_api.dart';
|
part 'api/authentication_api.dart';
|
||||||
part 'api/device_info_api.dart';
|
part 'api/device_info_api.dart';
|
||||||
|
part 'api/job_api.dart';
|
||||||
part 'api/server_info_api.dart';
|
part 'api/server_info_api.dart';
|
||||||
part 'api/user_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/admin_signup_response_dto.dart';
|
||||||
part 'model/album_count_response_dto.dart';
|
part 'model/album_count_response_dto.dart';
|
||||||
part 'model/album_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.dart';
|
||||||
part 'model/asset_count_by_time_bucket_response_dto.dart';
|
part 'model/asset_count_by_time_bucket_response_dto.dart';
|
||||||
part 'model/asset_count_by_user_id_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/exif_response_dto.dart';
|
||||||
part 'model/get_asset_by_time_bucket_dto.dart';
|
part 'model/get_asset_by_time_bucket_dto.dart';
|
||||||
part 'model/get_asset_count_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_credential_dto.dart';
|
||||||
part 'model/login_response_dto.dart';
|
part 'model/login_response_dto.dart';
|
||||||
part 'model/logout_response_dto.dart';
|
part 'model/logout_response_dto.dart';
|
||||||
|
|||||||
159
mobile/openapi/lib/api/job_api.dart
Normal file
159
mobile/openapi/lib/api/job_api.dart
Normal file
@@ -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<Response> getAllJobsStatusWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/jobs';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<AllJobStatusResponseDto?> 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<Response> 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 = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [JobId] jobId (required):
|
||||||
|
Future<JobStatusResponseDto?> 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<Response> 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 = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [JobId] jobId (required):
|
||||||
|
///
|
||||||
|
/// * [JobCommandDto] jobCommandDto (required):
|
||||||
|
Future<num?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,6 +202,8 @@ class ApiClient {
|
|||||||
return AlbumCountResponseDto.fromJson(value);
|
return AlbumCountResponseDto.fromJson(value);
|
||||||
case 'AlbumResponseDto':
|
case 'AlbumResponseDto':
|
||||||
return AlbumResponseDto.fromJson(value);
|
return AlbumResponseDto.fromJson(value);
|
||||||
|
case 'AllJobStatusResponseDto':
|
||||||
|
return AllJobStatusResponseDto.fromJson(value);
|
||||||
case 'AssetCountByTimeBucket':
|
case 'AssetCountByTimeBucket':
|
||||||
return AssetCountByTimeBucket.fromJson(value);
|
return AssetCountByTimeBucket.fromJson(value);
|
||||||
case 'AssetCountByTimeBucketResponseDto':
|
case 'AssetCountByTimeBucketResponseDto':
|
||||||
@@ -246,6 +248,16 @@ class ApiClient {
|
|||||||
return GetAssetByTimeBucketDto.fromJson(value);
|
return GetAssetByTimeBucketDto.fromJson(value);
|
||||||
case 'GetAssetCountByTimeBucketDto':
|
case 'GetAssetCountByTimeBucketDto':
|
||||||
return GetAssetCountByTimeBucketDto.fromJson(value);
|
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':
|
case 'LoginCredentialDto':
|
||||||
return LoginCredentialDto.fromJson(value);
|
return LoginCredentialDto.fromJson(value);
|
||||||
case 'LoginResponseDto':
|
case 'LoginResponseDto':
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is DeviceTypeEnum) {
|
if (value is DeviceTypeEnum) {
|
||||||
return DeviceTypeEnumTypeTransformer().encode(value).toString();
|
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) {
|
if (value is ThumbnailFormat) {
|
||||||
return ThumbnailFormatTypeTransformer().encode(value).toString();
|
return ThumbnailFormatTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
|||||||
167
mobile/openapi/lib/model/all_job_status_response_dto.dart
Normal file
167
mobile/openapi/lib/model/all_job_status_response_dto.dart
Normal file
@@ -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<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_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<String, dynamic>();
|
||||||
|
|
||||||
|
// 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<bool>(json, r'isThumbnailGenerationActive')!,
|
||||||
|
isMetadataExtractionActive: mapValueOfType<bool>(json, r'isMetadataExtractionActive')!,
|
||||||
|
isVideoConversionActive: mapValueOfType<bool>(json, r'isVideoConversionActive')!,
|
||||||
|
isMachineLearningActive: mapValueOfType<bool>(json, r'isMachineLearningActive')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<AllJobStatusResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AllJobStatusResponseDto>[];
|
||||||
|
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<String, AllJobStatusResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, AllJobStatusResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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<String, List<AllJobStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<AllJobStatusResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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 = <String>{
|
||||||
|
'thumbnailGenerationQueueCount',
|
||||||
|
'metadataExtractionQueueCount',
|
||||||
|
'videoConversionQueueCount',
|
||||||
|
'machineLearningQueueCount',
|
||||||
|
'isThumbnailGenerationActive',
|
||||||
|
'isMetadataExtractionActive',
|
||||||
|
'isVideoConversionActive',
|
||||||
|
'isMachineLearningActive',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -76,72 +76,69 @@ class AssetResponseDto {
|
|||||||
SmartInfoResponseDto? smartInfo;
|
SmartInfoResponseDto? smartInfo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
identical(this, other) ||
|
other.type == type &&
|
||||||
other is AssetResponseDto &&
|
other.id == id &&
|
||||||
other.type == type &&
|
other.deviceAssetId == deviceAssetId &&
|
||||||
other.id == id &&
|
other.ownerId == ownerId &&
|
||||||
other.deviceAssetId == deviceAssetId &&
|
other.deviceId == deviceId &&
|
||||||
other.ownerId == ownerId &&
|
other.originalPath == originalPath &&
|
||||||
other.deviceId == deviceId &&
|
other.resizePath == resizePath &&
|
||||||
other.originalPath == originalPath &&
|
other.createdAt == createdAt &&
|
||||||
other.resizePath == resizePath &&
|
other.modifiedAt == modifiedAt &&
|
||||||
other.createdAt == createdAt &&
|
other.isFavorite == isFavorite &&
|
||||||
other.modifiedAt == modifiedAt &&
|
other.mimeType == mimeType &&
|
||||||
other.isFavorite == isFavorite &&
|
other.duration == duration &&
|
||||||
other.mimeType == mimeType &&
|
other.webpPath == webpPath &&
|
||||||
other.duration == duration &&
|
other.encodedVideoPath == encodedVideoPath &&
|
||||||
other.webpPath == webpPath &&
|
other.exifInfo == exifInfo &&
|
||||||
other.encodedVideoPath == encodedVideoPath &&
|
other.smartInfo == smartInfo;
|
||||||
other.exifInfo == exifInfo &&
|
|
||||||
other.smartInfo == smartInfo;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(deviceAssetId.hashCode) +
|
(deviceAssetId.hashCode) +
|
||||||
(ownerId.hashCode) +
|
(ownerId.hashCode) +
|
||||||
(deviceId.hashCode) +
|
(deviceId.hashCode) +
|
||||||
(originalPath.hashCode) +
|
(originalPath.hashCode) +
|
||||||
(resizePath == null ? 0 : resizePath!.hashCode) +
|
(resizePath == null ? 0 : resizePath!.hashCode) +
|
||||||
(createdAt.hashCode) +
|
(createdAt.hashCode) +
|
||||||
(modifiedAt.hashCode) +
|
(modifiedAt.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(mimeType == null ? 0 : mimeType!.hashCode) +
|
(mimeType == null ? 0 : mimeType!.hashCode) +
|
||||||
(duration.hashCode) +
|
(duration.hashCode) +
|
||||||
(webpPath == null ? 0 : webpPath!.hashCode) +
|
(webpPath == null ? 0 : webpPath!.hashCode) +
|
||||||
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
|
||||||
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
(exifInfo == null ? 0 : exifInfo!.hashCode) +
|
||||||
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
(smartInfo == null ? 0 : smartInfo!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
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]';
|
||||||
'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<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
_json[r'type'] = type;
|
_json[r'type'] = type;
|
||||||
_json[r'id'] = id;
|
_json[r'id'] = id;
|
||||||
_json[r'deviceAssetId'] = deviceAssetId;
|
_json[r'deviceAssetId'] = deviceAssetId;
|
||||||
_json[r'ownerId'] = ownerId;
|
_json[r'ownerId'] = ownerId;
|
||||||
_json[r'deviceId'] = deviceId;
|
_json[r'deviceId'] = deviceId;
|
||||||
_json[r'originalPath'] = originalPath;
|
_json[r'originalPath'] = originalPath;
|
||||||
if (resizePath != null) {
|
if (resizePath != null) {
|
||||||
_json[r'resizePath'] = resizePath;
|
_json[r'resizePath'] = resizePath;
|
||||||
} else {
|
} else {
|
||||||
_json[r'resizePath'] = null;
|
_json[r'resizePath'] = null;
|
||||||
}
|
}
|
||||||
_json[r'createdAt'] = createdAt;
|
_json[r'createdAt'] = createdAt;
|
||||||
_json[r'modifiedAt'] = modifiedAt;
|
_json[r'modifiedAt'] = modifiedAt;
|
||||||
_json[r'isFavorite'] = isFavorite;
|
_json[r'isFavorite'] = isFavorite;
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
_json[r'mimeType'] = mimeType;
|
_json[r'mimeType'] = mimeType;
|
||||||
} else {
|
} else {
|
||||||
_json[r'mimeType'] = null;
|
_json[r'mimeType'] = null;
|
||||||
}
|
}
|
||||||
_json[r'duration'] = duration;
|
_json[r'duration'] = duration;
|
||||||
if (webpPath != null) {
|
if (webpPath != null) {
|
||||||
_json[r'webpPath'] = webpPath;
|
_json[r'webpPath'] = webpPath;
|
||||||
} else {
|
} else {
|
||||||
@@ -175,13 +172,13 @@ class AssetResponseDto {
|
|||||||
// Ensure that the map contains the required keys.
|
// Ensure that the map contains the required keys.
|
||||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||||
// Note 2: this code is stripped in release mode!
|
// Note 2: this code is stripped in release mode!
|
||||||
// assert(() {
|
assert(() {
|
||||||
// requiredKeys.forEach((key) {
|
requiredKeys.forEach((key) {
|
||||||
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
|
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.');
|
assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
|
||||||
// });
|
});
|
||||||
// return true;
|
return true;
|
||||||
// }());
|
}());
|
||||||
|
|
||||||
return AssetResponseDto(
|
return AssetResponseDto(
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
@@ -205,10 +202,7 @@ class AssetResponseDto {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AssetResponseDto>? listFromJson(
|
static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final result = <AssetResponseDto>[];
|
final result = <AssetResponseDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
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
|
// maps a json object with a list of AssetResponseDto-objects as value to a dart map
|
||||||
static Map<String, List<AssetResponseDto>> mapListFromJson(
|
static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
dynamic json, {
|
|
||||||
bool growable = false,
|
|
||||||
}) {
|
|
||||||
final map = <String, List<AssetResponseDto>>{};
|
final map = <String, List<AssetResponseDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AssetResponseDto.listFromJson(
|
final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
|
||||||
entry.value,
|
|
||||||
growable: growable,
|
|
||||||
);
|
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@@ -274,3 +262,4 @@ class AssetResponseDto {
|
|||||||
'encodedVideoPath',
|
'encodedVideoPath',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
111
mobile/openapi/lib/model/create_job_dto.dart
Normal file
111
mobile/openapi/lib/model/create_job_dto.dart
Normal file
@@ -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<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_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<String, dynamic>();
|
||||||
|
|
||||||
|
// 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<CreateJobDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <CreateJobDto>[];
|
||||||
|
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<String, CreateJobDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, CreateJobDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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<String, List<CreateJobDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<CreateJobDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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 = <String>{
|
||||||
|
'jobType',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@ class ExifResponseDto {
|
|||||||
/// Returns a new [ExifResponseDto] instance.
|
/// Returns a new [ExifResponseDto] instance.
|
||||||
ExifResponseDto({
|
ExifResponseDto({
|
||||||
this.id,
|
this.id,
|
||||||
|
this.fileSizeInByte,
|
||||||
this.make,
|
this.make,
|
||||||
this.model,
|
this.model,
|
||||||
this.imageName,
|
this.imageName,
|
||||||
this.exifImageWidth,
|
this.exifImageWidth,
|
||||||
this.exifImageHeight,
|
this.exifImageHeight,
|
||||||
this.fileSizeInByte,
|
|
||||||
this.orientation,
|
this.orientation,
|
||||||
this.dateTimeOriginal,
|
this.dateTimeOriginal,
|
||||||
this.modifyDate,
|
this.modifyDate,
|
||||||
@@ -35,7 +35,9 @@ class ExifResponseDto {
|
|||||||
this.country,
|
this.country,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? id;
|
int? id;
|
||||||
|
|
||||||
|
int? fileSizeInByte;
|
||||||
|
|
||||||
String? make;
|
String? make;
|
||||||
|
|
||||||
@@ -47,8 +49,6 @@ class ExifResponseDto {
|
|||||||
|
|
||||||
num? exifImageHeight;
|
num? exifImageHeight;
|
||||||
|
|
||||||
num? fileSizeInByte;
|
|
||||||
|
|
||||||
String? orientation;
|
String? orientation;
|
||||||
|
|
||||||
DateTime? dateTimeOriginal;
|
DateTime? dateTimeOriginal;
|
||||||
@@ -78,12 +78,12 @@ class ExifResponseDto {
|
|||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
|
other.fileSizeInByte == fileSizeInByte &&
|
||||||
other.make == make &&
|
other.make == make &&
|
||||||
other.model == model &&
|
other.model == model &&
|
||||||
other.imageName == imageName &&
|
other.imageName == imageName &&
|
||||||
other.exifImageWidth == exifImageWidth &&
|
other.exifImageWidth == exifImageWidth &&
|
||||||
other.exifImageHeight == exifImageHeight &&
|
other.exifImageHeight == exifImageHeight &&
|
||||||
other.fileSizeInByte == fileSizeInByte &&
|
|
||||||
other.orientation == orientation &&
|
other.orientation == orientation &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
other.modifyDate == modifyDate &&
|
other.modifyDate == modifyDate &&
|
||||||
@@ -102,12 +102,12 @@ class ExifResponseDto {
|
|||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(id == null ? 0 : id!.hashCode) +
|
(id == null ? 0 : id!.hashCode) +
|
||||||
|
(fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
|
||||||
(make == null ? 0 : make!.hashCode) +
|
(make == null ? 0 : make!.hashCode) +
|
||||||
(model == null ? 0 : model!.hashCode) +
|
(model == null ? 0 : model!.hashCode) +
|
||||||
(imageName == null ? 0 : imageName!.hashCode) +
|
(imageName == null ? 0 : imageName!.hashCode) +
|
||||||
(exifImageWidth == null ? 0 : exifImageWidth!.hashCode) +
|
(exifImageWidth == null ? 0 : exifImageWidth!.hashCode) +
|
||||||
(exifImageHeight == null ? 0 : exifImageHeight!.hashCode) +
|
(exifImageHeight == null ? 0 : exifImageHeight!.hashCode) +
|
||||||
(fileSizeInByte == null ? 0 : fileSizeInByte!.hashCode) +
|
|
||||||
(orientation == null ? 0 : orientation!.hashCode) +
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
|
||||||
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
(modifyDate == null ? 0 : modifyDate!.hashCode) +
|
||||||
@@ -123,7 +123,7 @@ class ExifResponseDto {
|
|||||||
(country == null ? 0 : country!.hashCode);
|
(country == null ? 0 : country!.hashCode);
|
||||||
|
|
||||||
@override
|
@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<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final _json = <String, dynamic>{};
|
final _json = <String, dynamic>{};
|
||||||
@@ -132,6 +132,11 @@ class ExifResponseDto {
|
|||||||
} else {
|
} else {
|
||||||
_json[r'id'] = null;
|
_json[r'id'] = null;
|
||||||
}
|
}
|
||||||
|
if (fileSizeInByte != null) {
|
||||||
|
_json[r'fileSizeInByte'] = fileSizeInByte;
|
||||||
|
} else {
|
||||||
|
_json[r'fileSizeInByte'] = null;
|
||||||
|
}
|
||||||
if (make != null) {
|
if (make != null) {
|
||||||
_json[r'make'] = make;
|
_json[r'make'] = make;
|
||||||
} else {
|
} else {
|
||||||
@@ -157,11 +162,6 @@ class ExifResponseDto {
|
|||||||
} else {
|
} else {
|
||||||
_json[r'exifImageHeight'] = null;
|
_json[r'exifImageHeight'] = null;
|
||||||
}
|
}
|
||||||
if (fileSizeInByte != null) {
|
|
||||||
_json[r'fileSizeInByte'] = fileSizeInByte;
|
|
||||||
} else {
|
|
||||||
_json[r'fileSizeInByte'] = null;
|
|
||||||
}
|
|
||||||
if (orientation != null) {
|
if (orientation != null) {
|
||||||
_json[r'orientation'] = orientation;
|
_json[r'orientation'] = orientation;
|
||||||
} else {
|
} else {
|
||||||
@@ -249,7 +249,8 @@ class ExifResponseDto {
|
|||||||
}());
|
}());
|
||||||
|
|
||||||
return ExifResponseDto(
|
return ExifResponseDto(
|
||||||
id: mapValueOfType<String>(json, r'id'),
|
id: mapValueOfType<int>(json, r'id'),
|
||||||
|
fileSizeInByte: mapValueOfType<int>(json, r'fileSizeInByte'),
|
||||||
make: mapValueOfType<String>(json, r'make'),
|
make: mapValueOfType<String>(json, r'make'),
|
||||||
model: mapValueOfType<String>(json, r'model'),
|
model: mapValueOfType<String>(json, r'model'),
|
||||||
imageName: mapValueOfType<String>(json, r'imageName'),
|
imageName: mapValueOfType<String>(json, r'imageName'),
|
||||||
@@ -259,9 +260,6 @@ class ExifResponseDto {
|
|||||||
exifImageHeight: json[r'exifImageHeight'] == null
|
exifImageHeight: json[r'exifImageHeight'] == null
|
||||||
? null
|
? null
|
||||||
: num.parse(json[r'exifImageHeight'].toString()),
|
: num.parse(json[r'exifImageHeight'].toString()),
|
||||||
fileSizeInByte: json[r'fileSizeInByte'] == null
|
|
||||||
? null
|
|
||||||
: num.parse(json[r'fileSizeInByte'].toString()),
|
|
||||||
orientation: mapValueOfType<String>(json, r'orientation'),
|
orientation: mapValueOfType<String>(json, r'orientation'),
|
||||||
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', ''),
|
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', ''),
|
||||||
modifyDate: mapDateTime(json, r'modifyDate', ''),
|
modifyDate: mapDateTime(json, r'modifyDate', ''),
|
||||||
|
|||||||
85
mobile/openapi/lib/model/job_command.dart
Normal file
85
mobile/openapi/lib/model/job_command.dart
Normal file
@@ -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 = <JobCommand>[
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
];
|
||||||
|
|
||||||
|
static JobCommand? fromJson(dynamic value) => JobCommandTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<JobCommand>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobCommand>[];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
111
mobile/openapi/lib/model/job_command_dto.dart
Normal file
111
mobile/openapi/lib/model/job_command_dto.dart
Normal file
@@ -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<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_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<String, dynamic>();
|
||||||
|
|
||||||
|
// 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<JobCommandDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobCommandDto>[];
|
||||||
|
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<String, JobCommandDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, JobCommandDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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<String, List<JobCommandDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<JobCommandDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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 = <String>{
|
||||||
|
'command',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
143
mobile/openapi/lib/model/job_counts.dart
Normal file
143
mobile/openapi/lib/model/job_counts.dart
Normal file
@@ -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<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_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<String, dynamic>();
|
||||||
|
|
||||||
|
// 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<int>(json, r'active')!,
|
||||||
|
completed: mapValueOfType<int>(json, r'completed')!,
|
||||||
|
failed: mapValueOfType<int>(json, r'failed')!,
|
||||||
|
delayed: mapValueOfType<int>(json, r'delayed')!,
|
||||||
|
waiting: mapValueOfType<int>(json, r'waiting')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<JobCounts>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobCounts>[];
|
||||||
|
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<String, JobCounts> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, JobCounts>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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<String, List<JobCounts>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<JobCounts>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // 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 = <String>{
|
||||||
|
'active',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'delayed',
|
||||||
|
'waiting',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
91
mobile/openapi/lib/model/job_id.dart
Normal file
91
mobile/openapi/lib/model/job_id.dart
Normal file
@@ -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 = <JobId>[
|
||||||
|
thumbnailGeneration,
|
||||||
|
metadataExtraction,
|
||||||
|
videoConversion,
|
||||||
|
machineLearning,
|
||||||
|
];
|
||||||
|
|
||||||
|
static JobId? fromJson(dynamic value) => JobIdTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<JobId>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobId>[];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
119
mobile/openapi/lib/model/job_status_response_dto.dart
Normal file
119
mobile/openapi/lib/model/job_status_response_dto.dart
Normal file
@@ -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<String, dynamic> toJson() {
|
||||||
|
final _json = <String, dynamic>{};
|
||||||
|
_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<String, dynamic>();
|
||||||
|
|
||||||
|
// 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<bool>(json, r'isActive')!,
|
||||||
|
queueCount: mapValueOfType<Object>(json, r'queueCount')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<JobStatusResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobStatusResponseDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = JobStatusResponseDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, JobStatusResponseDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, JobStatusResponseDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = JobStatusResponseDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of JobStatusResponseDto-objects as value to a dart map
|
||||||
|
static Map<String, List<JobStatusResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<JobStatusResponseDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = JobStatusResponseDto.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 = <String>{
|
||||||
|
'isActive',
|
||||||
|
'queueCount',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
91
mobile/openapi/lib/model/job_type.dart
Normal file
91
mobile/openapi/lib/model/job_type.dart
Normal file
@@ -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 JobType {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const JobType._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const THUMBNAIL_GENERATION = JobType._(r'THUMBNAIL_GENERATION');
|
||||||
|
static const METADATA_EXTRACTION = JobType._(r'METADATA_EXTRACTION');
|
||||||
|
static const VIDEO_CONVERSION = JobType._(r'VIDEO_CONVERSION');
|
||||||
|
static const CHECKSUM_GENERATION = JobType._(r'CHECKSUM_GENERATION');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][JobType].
|
||||||
|
static const values = <JobType>[
|
||||||
|
THUMBNAIL_GENERATION,
|
||||||
|
METADATA_EXTRACTION,
|
||||||
|
VIDEO_CONVERSION,
|
||||||
|
CHECKSUM_GENERATION,
|
||||||
|
];
|
||||||
|
|
||||||
|
static JobType? fromJson(dynamic value) => JobTypeTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<JobType>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <JobType>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = JobType.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [JobType] to String,
|
||||||
|
/// and [decode] dynamic data back to [JobType].
|
||||||
|
class JobTypeTypeTransformer {
|
||||||
|
factory JobTypeTypeTransformer() => _instance ??= const JobTypeTypeTransformer._();
|
||||||
|
|
||||||
|
const JobTypeTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(JobType data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a JobType.
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
JobType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data.toString()) {
|
||||||
|
case r'THUMBNAIL_GENERATION': return JobType.THUMBNAIL_GENERATION;
|
||||||
|
case r'METADATA_EXTRACTION': return JobType.METADATA_EXTRACTION;
|
||||||
|
case r'VIDEO_CONVERSION': return JobType.VIDEO_CONVERSION;
|
||||||
|
case r'CHECKSUM_GENERATION': return JobType.CHECKSUM_GENERATION;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [JobTypeTypeTransformer] instance.
|
||||||
|
static JobTypeTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
52
mobile/openapi/test/all_job_status_response_dto_test.dart
Normal file
52
mobile/openapi/test/all_job_status_response_dto_test.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for AllJobStatusResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = AllJobStatusResponseDto();
|
||||||
|
|
||||||
|
group('test AllJobStatusResponseDto', () {
|
||||||
|
// bool isThumbnailGenerationActive
|
||||||
|
test('to test the property `isThumbnailGenerationActive`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object thumbnailGenerationQueueCount
|
||||||
|
test('to test the property `thumbnailGenerationQueueCount`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isMetadataExtractionActive
|
||||||
|
test('to test the property `isMetadataExtractionActive`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object metadataExtractionQueueCount
|
||||||
|
test('to test the property `metadataExtractionQueueCount`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// bool isVideoConversionActive
|
||||||
|
test('to test the property `isVideoConversionActive`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object videoConversionQueueCount
|
||||||
|
test('to test the property `videoConversionQueueCount`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
27
mobile/openapi/test/create_job_dto_test.dart
Normal file
27
mobile/openapi/test/create_job_dto_test.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for CreateJobDto
|
||||||
|
void main() {
|
||||||
|
// final instance = CreateJobDto();
|
||||||
|
|
||||||
|
group('test CreateJobDto', () {
|
||||||
|
// JobType jobType
|
||||||
|
test('to test the property `jobType`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
41
mobile/openapi/test/job_api_test.dart
Normal file
41
mobile/openapi/test/job_api_test.dart
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
|
||||||
|
/// tests for JobApi
|
||||||
|
void main() {
|
||||||
|
// final instance = JobApi();
|
||||||
|
|
||||||
|
group('tests for JobApi', () {
|
||||||
|
//Future<Object> create(CreateJobDto createJobDto) async
|
||||||
|
test('test create', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
//Future<AllJobStatusResponseDto> getAllJobsStatus() async
|
||||||
|
test('test getAllJobsStatus', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
//Future<JobStatusResponseDto> getJobStatus(JobType jobType) async
|
||||||
|
test('test getJobStatus', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
//Future<JobStatusResponseDto> stopJob(JobType jobType) async
|
||||||
|
test('test stopJob', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
27
mobile/openapi/test/job_command_dto_test.dart
Normal file
27
mobile/openapi/test/job_command_dto_test.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobCommandDto
|
||||||
|
void main() {
|
||||||
|
// final instance = JobCommandDto();
|
||||||
|
|
||||||
|
group('test JobCommandDto', () {
|
||||||
|
// JobCommand command
|
||||||
|
test('to test the property `command`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
21
mobile/openapi/test/job_command_test.dart
Normal file
21
mobile/openapi/test/job_command_test.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobCommand
|
||||||
|
void main() {
|
||||||
|
|
||||||
|
group('test JobCommand', () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
47
mobile/openapi/test/job_counts_test.dart
Normal file
47
mobile/openapi/test/job_counts_test.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobCounts
|
||||||
|
void main() {
|
||||||
|
// final instance = JobCounts();
|
||||||
|
|
||||||
|
group('test JobCounts', () {
|
||||||
|
// num active
|
||||||
|
test('to test the property `active`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num completed
|
||||||
|
test('to test the property `completed`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num failed
|
||||||
|
test('to test the property `failed`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num delayed
|
||||||
|
test('to test the property `delayed`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// num waiting
|
||||||
|
test('to test the property `waiting`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
21
mobile/openapi/test/job_id_test.dart
Normal file
21
mobile/openapi/test/job_id_test.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobId
|
||||||
|
void main() {
|
||||||
|
|
||||||
|
group('test JobId', () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
32
mobile/openapi/test/job_status_response_dto_test.dart
Normal file
32
mobile/openapi/test/job_status_response_dto_test.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobStatusResponseDto
|
||||||
|
void main() {
|
||||||
|
// final instance = JobStatusResponseDto();
|
||||||
|
|
||||||
|
group('test JobStatusResponseDto', () {
|
||||||
|
// bool isActive
|
||||||
|
test('to test the property `isActive`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
// Object queueCount
|
||||||
|
test('to test the property `queueCount`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
21
mobile/openapi/test/job_type_test.dart
Normal file
21
mobile/openapi/test/job_type_test.dart
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// 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
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
// tests for JobType
|
||||||
|
void main() {
|
||||||
|
|
||||||
|
group('test JobType', () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
@@ -868,6 +868,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.27.3"
|
version: "0.27.3"
|
||||||
|
scrollable_positioned_list:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: scrollable_positioned_list
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 1.29.1+43
|
version: 1.31.0+49
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0 <3.0.0"
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
@@ -43,6 +43,7 @@ dependencies:
|
|||||||
easy_localization: ^3.0.1
|
easy_localization: ^3.0.1
|
||||||
share_plus: ^4.0.10
|
share_plus: ^4.0.10
|
||||||
flutter_displaymode: ^0.4.0
|
flutter_displaymode: ^0.4.0
|
||||||
|
scrollable_positioned_list: ^0.3.4
|
||||||
|
|
||||||
path: ^1.8.1
|
path: ^1.8.1
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
upload/
|
upload/
|
||||||
dist/
|
dist/
|
||||||
|
.reverse-geocoding-dump
|
||||||
|
|||||||
1
server/.gitignore
vendored
1
server/.gitignore
vendored
@@ -39,3 +39,4 @@ dist/
|
|||||||
upload/
|
upload/
|
||||||
tmp/
|
tmp/
|
||||||
core
|
core
|
||||||
|
.reverse-geocoding-dump/
|
||||||
|
|||||||
@@ -29,4 +29,6 @@ COPY --from=builder /usr/src/app/dist ./dist
|
|||||||
|
|
||||||
RUN npm prune --production
|
RUN npm prune --production
|
||||||
|
|
||||||
|
VOLUME /usr/src/app/upload
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ describe('Album service', () => {
|
|||||||
getAssetByTimeBucket: jest.fn(),
|
getAssetByTimeBucket: jest.fn(),
|
||||||
getAssetByChecksum: jest.fn(),
|
getAssetByChecksum: jest.fn(),
|
||||||
getAssetCountByUserId: jest.fn(),
|
getAssetCountByUserId: jest.fn(),
|
||||||
|
getAssetWithNoEXIF: jest.fn(),
|
||||||
|
getAssetWithNoThumbnail: jest.fn(),
|
||||||
|
getAssetWithNoSmartInfo: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock);
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export interface IAssetRepository {
|
|||||||
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||||
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
||||||
|
getAssetWithNoThumbnail(): Promise<AssetEntity[]>;
|
||||||
|
getAssetWithNoEXIF(): Promise<AssetEntity[]>;
|
||||||
|
getAssetWithNoSmartInfo(): Promise<AssetEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
|
||||||
@@ -40,6 +43,33 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async getAssetWithNoSmartInfo(): Promise<AssetEntity[]> {
|
||||||
|
return await this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.leftJoinAndSelect('asset.smartInfo', 'si')
|
||||||
|
.where('asset.resizePath IS NOT NULL')
|
||||||
|
.andWhere('si.id IS NULL')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAssetWithNoThumbnail(): Promise<AssetEntity[]> {
|
||||||
|
return await this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.where('asset.resizePath IS NULL')
|
||||||
|
.orWhere('asset.resizePath = :resizePath', { resizePath: '' })
|
||||||
|
.orWhere('asset.webpPath IS NULL')
|
||||||
|
.orWhere('asset.webpPath = :webpPath', { webpPath: '' })
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAssetWithNoEXIF(): Promise<AssetEntity[]> {
|
||||||
|
return await this.assetRepository
|
||||||
|
.createQueryBuilder('asset')
|
||||||
|
.leftJoinAndSelect('asset.exifInfo', 'ei')
|
||||||
|
.where('ei."assetId" IS NULL')
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
|
async getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto> {
|
||||||
// Get asset count by AssetType
|
// Get asset count by AssetType
|
||||||
const res = await this.assetRepository
|
const res = await this.assetRepository
|
||||||
@@ -171,7 +201,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
.createQueryBuilder('asset')
|
.createQueryBuilder('asset')
|
||||||
.where('asset.userId = :userId', { userId: userId })
|
.where('asset.userId = :userId', { userId: userId })
|
||||||
.andWhere('asset.resizePath is not NULL')
|
.andWhere('asset.resizePath is not NULL')
|
||||||
.andWhere('asset.type = :type', { type: AssetType.IMAGE })
|
|
||||||
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
||||||
.orderBy('asset.createdAt', 'DESC');
|
.orderBy('asset.createdAt', 'DESC');
|
||||||
|
|
||||||
@@ -226,7 +255,6 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
where: {
|
where: {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
type: AssetType.IMAGE,
|
|
||||||
},
|
},
|
||||||
select: ['deviceAssetId'],
|
select: ['deviceAssetId'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { CommunicationGateway } from '../communication/communication.gateway';
|
|||||||
import { InjectQueue } from '@nestjs/bull';
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
import { Queue } from 'bull';
|
import { Queue } from 'bull';
|
||||||
import { IAssetUploadedJob } from '@app/job/index';
|
import { IAssetUploadedJob } from '@app/job/index';
|
||||||
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
||||||
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||||
@@ -59,7 +59,7 @@ export class AssetController {
|
|||||||
private assetService: AssetService,
|
private assetService: AssetService,
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
private backgroundTaskService: BackgroundTaskService,
|
||||||
|
|
||||||
@InjectQueue(assetUploadedQueueName)
|
@InjectQueue(QueueNameEnum.ASSET_UPLOADED)
|
||||||
private assetUploadedQueue: Queue<IAssetUploadedJob>,
|
private assetUploadedQueue: Queue<IAssetUploadedJob>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ export class AssetController {
|
|||||||
|
|
||||||
await this.assetUploadedQueue.add(
|
await this.assetUploadedQueue.add(
|
||||||
assetUploadedProcessorName,
|
assetUploadedProcessorName,
|
||||||
{ asset: savedAsset, fileName: file.originalname, fileSize: file.size },
|
{ asset: savedAsset, fileName: file.originalname },
|
||||||
{ jobId: savedAsset.id },
|
{ jobId: savedAsset.id },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { BullModule } from '@nestjs/bull';
|
|||||||
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
||||||
import { CommunicationModule } from '../communication/communication.module';
|
import { CommunicationModule } from '../communication/communication.module';
|
||||||
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
|
import { QueueNameEnum } from '@app/job/constants/queue-name.constant';
|
||||||
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
|
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -16,7 +16,7 @@ import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
|
|||||||
BackgroundTaskModule,
|
BackgroundTaskModule,
|
||||||
TypeOrmModule.forFeature([AssetEntity]),
|
TypeOrmModule.forFeature([AssetEntity]),
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: assetUploadedQueueName,
|
name: QueueNameEnum.ASSET_UPLOADED,
|
||||||
defaultJobOptions: {
|
defaultJobOptions: {
|
||||||
attempts: 3,
|
attempts: 3,
|
||||||
removeOnComplete: true,
|
removeOnComplete: true,
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ describe('AssetService', () => {
|
|||||||
getAssetByTimeBucket: jest.fn(),
|
getAssetByTimeBucket: jest.fn(),
|
||||||
getAssetByChecksum: jest.fn(),
|
getAssetByChecksum: jest.fn(),
|
||||||
getAssetCountByUserId: jest.fn(),
|
getAssetCountByUserId: jest.fn(),
|
||||||
|
getAssetWithNoEXIF: jest.fn(),
|
||||||
|
getAssetWithNoThumbnail: jest.fn(),
|
||||||
|
getAssetWithNoSmartInfo: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sui = new AssetService(assetRepositoryMock, a);
|
sui = new AssetService(assetRepositoryMock, a);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { ExifEntity } from '@app/database/entities/exif.entity';
|
import { ExifEntity } from '@app/database/entities/exif.entity';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class ExifResponseDto {
|
export class ExifResponseDto {
|
||||||
id?: string | null = null;
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
|
id?: number | null = null;
|
||||||
make?: string | null = null;
|
make?: string | null = null;
|
||||||
model?: string | null = null;
|
model?: string | null = null;
|
||||||
imageName?: string | null = null;
|
imageName?: string | null = null;
|
||||||
exifImageWidth?: number | null = null;
|
exifImageWidth?: number | null = null;
|
||||||
exifImageHeight?: number | null = null;
|
exifImageHeight?: number | null = null;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
fileSizeInByte?: number | null = null;
|
fileSizeInByte?: number | null = null;
|
||||||
orientation?: string | null = null;
|
orientation?: string | null = null;
|
||||||
dateTimeOriginal?: Date | null = null;
|
dateTimeOriginal?: Date | null = null;
|
||||||
@@ -25,13 +29,13 @@ export class ExifResponseDto {
|
|||||||
|
|
||||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: parseInt(entity.id),
|
||||||
make: entity.make,
|
make: entity.make,
|
||||||
model: entity.model,
|
model: entity.model,
|
||||||
imageName: entity.imageName,
|
imageName: entity.imageName,
|
||||||
exifImageWidth: entity.exifImageWidth,
|
exifImageWidth: entity.exifImageWidth,
|
||||||
exifImageHeight: entity.exifImageHeight,
|
exifImageHeight: entity.exifImageHeight,
|
||||||
fileSizeInByte: entity.fileSizeInByte,
|
fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
|
||||||
orientation: entity.orientation,
|
orientation: entity.orientation,
|
||||||
dateTimeOriginal: entity.dateTimeOriginal,
|
dateTimeOriginal: entity.dateTimeOriginal,
|
||||||
modifyDate: entity.modifyDate,
|
modifyDate: entity.modifyDate,
|
||||||
|
|||||||
21
server/apps/immich/src/api-v1/job/dto/get-job.dto.ts
Normal file
21
server/apps/immich/src/api-v1/job/dto/get-job.dto.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export enum JobId {
|
||||||
|
THUMBNAIL_GENERATION = 'thumbnail-generation',
|
||||||
|
METADATA_EXTRACTION = 'metadata-extraction',
|
||||||
|
VIDEO_CONVERSION = 'video-conversion',
|
||||||
|
MACHINE_LEARNING = 'machine-learning',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetJobDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(JobId, {
|
||||||
|
message: `params must be one of ${Object.values(JobId).join()}`,
|
||||||
|
})
|
||||||
|
@ApiProperty({
|
||||||
|
enum: JobId,
|
||||||
|
enumName: 'JobId',
|
||||||
|
})
|
||||||
|
jobId!: string;
|
||||||
|
}
|
||||||
12
server/apps/immich/src/api-v1/job/dto/job-command.dto.ts
Normal file
12
server/apps/immich/src/api-v1/job/dto/job-command.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsIn, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class JobCommandDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsIn(['start', 'stop'])
|
||||||
|
@ApiProperty({
|
||||||
|
enum: ['start', 'stop'],
|
||||||
|
enumName: 'JobCommand',
|
||||||
|
})
|
||||||
|
command!: string;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user