Compare commits

...

27 Commits

Author SHA1 Message Date
Jason Rasmussen 43c8b157dc feat: plugins 2026-05-13 10:27:21 -04:00
Daniel Dietzler 6c4502ff94 chore: better types 2026-05-12 15:47:23 -04:00
Jason Rasmussen f35b30f1d9 WIP 2026-05-12 15:47:23 -04:00
Jason Rasmussen 58beac8fe0 chore: migrate mobile makefile to mise (#28390) 2026-05-12 15:21:04 -04:00
Santo Shakil f632d320f5 fix(mobile): clear linkedRemoteAlbumId in reset() so FK refs dont dangle (#28382)
* fix(mobile): clear linkedRemoteAlbumId in reset() so FK refs dont dangle

reset() runs with foreign_keys off before wiping remote_* tables, so the ON DELETE SET NULL cascade on linkedRemoteAlbumId doesnt fire. local rows keep pointing at deleted remote ids.

affects logout (clearLocalData calls reset()) and the server SyncResetV1 path (30 day idle, etc). after re-login, syncLinkedAlbum either silently warns or fires 400s (those are covered by #28299).

null the column manually inside the same transaction. cascade still works for normal SyncAlbumDeleteV1.

verified on pixel 9a with this branch built locally: logged out, deleted album from web, logged back in. without fix linkedRemoteAlbumId stayed dangling. with fix all three local rows have linkedRemoteAlbumId = NULL after the logout reset, and recovery is clean once manageLinkedAlbums runs again.

* fix(mobile): always re-enable foreign_keys in reset() + simplify the update

re-enable foreign_keys inside a try/finally so it always runs even if the transaction throws. without this, a failed reset would leave the connection with foreign_keys = OFF and silently disable cascades for everything after (per copilot review).

also drop the where filter on the linkedRemoteAlbumId update, unconditional update-all is simpler and we wipe everything in reset anyway (per ganka review).
2026-05-12 13:43:15 -05:00
shenlong 2ddaf6a611 fix: indexes on remote_asset_entity (#28264)
* fix: periodically execute pragma optimize

* fix: indexes on remote_asset_entity

* regen files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-12 16:43:24 +00:00
shenlong 1932c60e1c fix: kekab icon colors in light mode (#28366)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-12 11:27:55 -05:00
Brandon Wees dc6f8e746e fix: deep link for assets when asset viewer already open (#27971) 2026-05-12 16:19:54 +00:00
Jason Rasmussen ad7aedb843 refactor: move plugins to packages (#28389) 2026-05-12 13:28:30 +00:00
Santo Shakil 571e6a8560 chore(mobile): drop deprecated deviceAssetId / deviceId from upload fields (#28384)
server removed both fields from AssetMediaCreateDto in #27818. zod silently strips unknown fields so uploads still work, but we send dead weight on every request.

drop from foreground + background upload paths + share intent path. deviceAssetId stays as the internal background_downloader taskId, just not in the multipart form fields anymore.
2026-05-12 09:12:26 -04:00
bo0tzz 4791313def fix: manage oazapfts through mise (#28380) 2026-05-12 08:12:27 -04:00
renovate[bot] f88fdae048 fix(deps): update dependency @immich/ui to ^0.77.0 (#28373)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 12:15:47 +02:00
renovate[bot] bcef7aa6b6 chore(deps): update github-actions (#28372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 12:07:32 +02:00
renovate[bot] ce292bdce9 chore(deps): update base-image to v202605051129 (#28374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 12:02:43 +02:00
renovate[bot] 4eee023648 chore(deps): update docker.io/valkey/valkey:9 docker digest to 8436e10 (#28369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 11:52:30 +02:00
shenlong 8f4b0fce49 fix: limit android background worker duration (#23566)
* fix: limit each android background run to 20 mins

# Conflicts:
#	mobile/lib/domain/services/background_worker.service.dart

* review chages

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-11 23:08:17 -05:00
Timon c6b3127b35 feat(web): add individual filter removal from search result chips (#28166)
* feat(web): add individual filter removal from search result chips

* drop cast

* use delete

* lint

* stylings

* filter

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-12 03:50:40 +00:00
shenlong 4d6a50c2cb refactor: move map config to metadata table (#28226)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

* migrate primary color

* migrate dynamic theme

* migrate colorfulInterface

* cleanup providers

* migrate cleanup

* migrate mapconfig

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-11 22:43:52 -05:00
Jason Rasmussen 15f3947ae6 chore: mise scripts (#28367) 2026-05-11 17:46:02 -04:00
Ben Beckford e142e3aca7 feat: recently added assets page (#28272)
* feat(server): add ordering date option to time buckets

* feat(web): add recently added page

* feat(server): recently created assets in explore data

* feat(web): recently added in explore tab

* fix: recently added assets ordering

* fix(server): failing bucket test

* feat(web): improve recently added preview

* chore: update e2e explore/timeline tests

* chore: rename and refactor timeline ordering dates

* fix(web): invalid timeline option

* feat(mobile): recently added page

* fix(server): sync tests

* fix(mobile): resync assets to get uploadedAt column

* chore: rename assetorderby enum

* chore(mobile): formatting

* minor fixes

* stylings

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-11 21:35:10 +00:00
Brandon Wees 38438c8d9a refactor!: remove asset faces from AssetResponseDto (#27779)
* refactor!: remove faces from `people` in AssetResposnseDto

* chore: tests

* chore: e2e generator

* chore: code review readonly

* chore: code review changes

* chore: cleanup

* fix: openapi

* chore: format

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-11 17:05:40 -04:00
Alex a278c10c75 fix: mobile upload duration type (#28362) 2026-05-11 15:46:00 -05:00
renovate[bot] 2276443c56 fix(deps): update dependency kysely to v0.28.17 [security] (#28363)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 22:38:25 +02:00
Jason Rasmussen bb44773e57 chore: remove unused commands (#28361) 2026-05-11 16:19:40 -04:00
Jason Rasmussen 14d9e90a03 refactor: move i18n formatting to workspace root (#28360)
refactor: move i18n formatting to project root
2026-05-11 16:19:28 -04:00
Jason Rasmussen 03e042213c refactor: move e2e-auth-server to packages (#28358) 2026-05-11 15:39:59 -04:00
Jason Rasmussen db589455f4 refactor: move cli to package folder (#28356) 2026-05-11 14:49:45 -04:00
297 changed files with 20809 additions and 9401 deletions
+1 -1
View File
@@ -75,7 +75,7 @@
{
"label": "Build Immich CLI",
"type": "shell",
"command": "pnpm --filter cli build:dev"
"command": "pnpm --filter @immich/cli build:dev"
}
]
}
@@ -16,7 +16,7 @@ services:
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
- /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store
- ../plugins:/build/corePlugin
- ../packages/plugin-core:/build/plugins/immich-plugin-core
immich-web:
env_file: !reset []
immich-machine-learning:
+1 -1
View File
@@ -1,7 +1,7 @@
cli:
- changed-files:
- any-glob-to-any-file:
- cli/src/**
- packages/cli/src/**
documentation:
- changed-files:
+18 -19
View File
@@ -90,6 +90,11 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
github_token: ${{ steps.token.outputs.token }}
- name: Create the Keystore
if: ${{ !github.event.pull_request.head.repo.fork }}
env:
@@ -114,13 +119,6 @@ jobs:
mobile/.dart_tool
key: build-mobile-gradle-${{ runner.os }}-main
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
cache: true
- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
with:
@@ -131,11 +129,10 @@ jobs:
run: flutter pub get
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
working-directory: ./mobile
run: mise //mobile:codegen:translation
- name: Generate platform APIs
run: make pigeon
run: mise //mobile:codegen:pigeon
working-directory: ./mobile
- name: Build Android App Bundle
@@ -205,6 +202,12 @@ jobs:
runs-on: macos-15
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.2.app/Contents/Developer
@@ -214,24 +217,20 @@ jobs:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
cache: true
github_token: ${{ steps.token.outputs.token }}
- name: Install Flutter dependencies
working-directory: ./mobile
run: flutter pub get
- name: Generate translation files
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
working-directory: ./mobile
run: mise //mobile:codegen:translation
- name: Generate platform APIs
run: make pigeon
working-directory: ./mobile
run: mise //mobile:codegen:pigeon
- name: Setup Ruby
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
+5 -5
View File
@@ -3,11 +3,11 @@ on:
push:
branches: [main]
paths:
- 'cli/**'
- 'packages/cli/**'
- '.github/workflows/cli.yml'
pull_request:
paths:
- 'cli/**'
- 'packages/cli/**'
- '.github/workflows/cli.yml'
release:
types: [published]
@@ -28,7 +28,7 @@ jobs:
packages: write
defaults:
run:
working-directory: ./cli
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -89,7 +89,7 @@ jobs:
- name: Get package version
id: package-version
run: |
version=$(jq -r '.version' cli/package.json)
version=$(jq -r '.version' packages/cli/package.json)
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Generate docker image tags
@@ -107,7 +107,7 @@ jobs:
- name: Build and push image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
file: cli/Dockerfile
file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'release' }}
cache-from: type=gha
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
image: ghcr.io/immich-app/mdq:main@sha256:0a8b8867773a0f8368061f47578603f438349f8f1f28b0e16105f481e5c794e0
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:
+3 -3
View File
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# 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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
# ️ 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
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: '/language:${{matrix.language}}'
+1 -1
View File
@@ -17,6 +17,6 @@ jobs:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: ${{ steps.token.outputs.token }}
+15 -27
View File
@@ -60,38 +60,30 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: dart pub get
run: flutter pub get
- name: Install dependencies for UI package
run: dart pub get
run: flutter pub get
working-directory: ./mobile/packages/ui
- name: Install dependencies for UI Showcase
run: dart pub get
run: flutter pub get
working-directory: ./mobile/packages/ui/showcase
- name: Install DCM
uses: CQLabs/setup-dcm@8697ae0790c0852e964a6ef1d768d62a6675481a # v2.0.1
with:
github-token: ${{ steps.token.outputs.token }}
version: auto
working-directory: ./mobile
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Run Build Runner
run: make build
run: mise //mobile:codegen:dart
- name: Generate platform API
run: make pigeon
run: mise //mobile:codegen:pigeon
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -107,20 +99,16 @@ jobs:
env:
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
run: |
echo "ERROR: Generated files not up to date! Run 'make build' and 'make pigeon' inside the mobile directory"
echo "ERROR: Generated files not up to date! Run 'mise //mobile:codegen:dart' and 'mise //mobile:codegen:pigeon'"
echo "Changed files: ${CHANGED_FILES}"
exit 1
- name: Run dart analyze
run: dart analyze --fatal-infos
- name: Run analyze
run: mise //mobile:analyze
- name: Run dart format
run: make format
- name: Run format
run: mise //mobile:format
# TODO: Re-enable after upgrading custom_lint
# - name: Run dart custom_lint
# run: dart run custom_lint
# TODO: Use https://github.com/CQLabs/dcm-action
- name: Run DCM
run: dcm analyze lib --fatal-style --fatal-warnings
+26 -27
View File
@@ -39,7 +39,7 @@ jobs:
- 'server/**'
- 'pnpm-lock.yaml'
cli:
- 'cli/**'
- 'packages/cli/**'
- 'packages/sdk/**'
- 'pnpm-lock.yaml'
e2e:
@@ -95,7 +95,7 @@ jobs:
contents: read
defaults:
run:
working-directory: ./cli
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -126,7 +126,7 @@ jobs:
contents: read
defaults:
run:
working-directory: ./cli
working-directory: ./packages/cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
@@ -256,10 +256,10 @@ jobs:
github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile
run: pnpm -w install --frozen-lockfile
- name: Format
run: pnpm --filter=immich-i18n format:fix
run: pnpm format:fix
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
@@ -379,19 +379,14 @@ jobs:
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup @immich/sdk
run: pnpm --filter @immich/sdk install --frozen-lockfile && pnpm --filter @immich/sdk build
- name: Setup packages
run: pnpm --filter "@immich/*" install --frozen-lockfile && pnpm --filter "@immich/*" build
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
@@ -556,17 +551,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Flutter SDK
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
- name: Generate translation file
run: dart run easy_localization:generate -S ../i18n && dart run bin/generate_keys.dart
github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: flutter pub get
working-directory: ./mobile
- name: Generate translation files
run: mise //mobile:codegen:translation
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
run: mise //mobile:test -j 1
ml-unit-tests:
name: Unit Test ML
needs: pre-job
@@ -678,12 +678,8 @@ jobs:
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- name: Build the app
run: pnpm --filter immich build
- name: Run API generation
run: ./bin/generate-open-api.sh
run: mise //:open-api
working-directory: open-api
- name: Find file changes
@@ -744,8 +740,11 @@ jobs:
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Build plugins
run: mise run //:plugins
- name: Build the app
run: pnpm build
run: mise build
- name: Run existing migrations
run: pnpm migrations:run
@@ -775,7 +774,7 @@ jobs:
exit 1
- name: Run SQL generation
run: pnpm sync:sql
run: mise //:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
github-token: ${{ steps.token.outputs.token }}
filters: |
i18n:
- modified: 'i18n/!(en|package)**\.json'
- modified: 'i18n/!(en)**\.json'
skip-force-logic: 'true'
enforce-lock:
View File
+6 -4
View File
@@ -23,15 +23,17 @@
"type": "node",
"request": "launch",
"name": "Immich CLI",
"program": "${workspaceFolder}/cli/dist/index.js",
"program": "${workspaceFolder}/packages/cli/dist/index.js",
"args": ["upload", "--help"],
"runtimeArgs": ["--enable-source-maps"],
"console": "integratedTerminal",
"resolveSourceMapLocations": ["${workspaceFolder}/cli/dist/**/*.js.map"],
"resolveSourceMapLocations": [
"${workspaceFolder}/packages/cli/dist/**/*.js.map"
],
"sourceMaps": true,
"outFiles": ["${workspaceFolder}/cli/dist/**/*.js"],
"outFiles": ["${workspaceFolder}/packages/cli/dist/**/*.js"],
"skipFiles": ["<node_internals>/**"],
"preLaunchTask": "Build Immich CLI"
"preLaunchTask": "Build @immich/cli"
}
]
}
+2 -87
View File
@@ -37,105 +37,24 @@ prod-scale:
.PHONY: open-api
open-api:
cd ./open-api && bash ./bin/generate-open-api.sh
open-api-dart:
cd ./open-api && bash ./bin/generate-open-api.sh dart
open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
sql:
pnpm --filter immich run sync:sql
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
attach-server:
docker exec -it docker_immich-server_1 sh
renovate:
LOG_LEVEL=debug pnpm exec renovate --platform=local --repository-cache=reset
# Directories that need to be created for volumes or build output
VOLUME_DIRS = \
./.pnpm-store \
./web/.svelte-kit \
./web/node_modules \
./web/coverage \
./e2e/node_modules \
./docs/node_modules \
./server/node_modules \
./packages/sdk/node_modules \
./.github/node_modules \
./node_modules \
./cli/node_modules
# Include .env file if it exists
-include docker/.env
MODULES = e2e server web cli sdk docs .github
# directory to package name mapping function
# cli = @immich/cli
# docs = documentation
# e2e = immich-e2e
# packages/sdk = @immich/sdk
# server = immich
# web = immich-web
map-package = $(subst sdk,@immich/sdk,$(subst cli,@immich/cli,$(subst docs,documentation,$(subst e2e,immich-e2e,$(subst server,immich,$(subst web,immich-web,$1))))))
audit-%:
pnpm --filter $(call map-package,$*) audit fix
install-%:
pnpm --filter $(call map-package,$*) install $(if $(FROZEN),--frozen-lockfile) $(if $(OFFLINE),--offline)
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
pnpm --filter $(call map-package,$*) run build
format-%:
pnpm --filter $(call map-package,$*) run format:fix
lint-%:
pnpm --filter $(call map-package,$*) run lint:fix
check-%:
pnpm --filter $(call map-package,$*) run check
check-web:
pnpm --filter immich-web run check:typescript
pnpm --filter immich-web run check:svelte
test-%:
pnpm --filter $(call map-package,$*) run test
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
pnpm --filter immich-e2e run test
pnpm --filter immich-e2e run test:web
test-medium:
docker run \
--rm \
-v ./server/src:/usr/src/app/src \
-v ./server/test:/usr/src/app/test \
-v ./server/vitest.config.medium.mjs:/usr/src/app/vitest.config.medium.mjs \
-v ./server/tsconfig.json:/usr/src/app/tsconfig.json \
-e NODE_ENV=development \
immich-server:latest \
-c "pnpm test:medium -- --run"
test-medium-dev:
docker exec -it immich_server /bin/sh -c "pnpm run test:medium"
install-all:
pnpm -r --filter '!documentation' install
build-all: $(foreach M,$(filter-out e2e docs .github,$(MODULES)),build-$M) ;
check-all:
pnpm -r --filter '!documentation' run "/^(check|check\:svelte|check\:typescript)$/"
lint-all:
pnpm -r --filter '!documentation' run lint:fix
format-all:
pnpm -r --filter '!documentation' run format:fix
audit-all:
pnpm -r --filter '!documentation' audit fix
hygiene-all: audit-all
pnpm -r --filter '!documentation' run "/(format:fix|check|check:svelte|check:typescript|sql)/"
test-all:
pnpm -r --filter '!documentation' run "/^test/"
clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} +
@@ -146,7 +65,3 @@ clean:
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
setup-server-dev: install-server
setup-web-dev: install-sdk build-sdk install-web
+3 -3
View File
@@ -25,7 +25,7 @@ services:
- server_node_modules:/usr/src/app/server/node_modules
- web_node_modules:/usr/src/app/web/node_modules
- github_node_modules:/usr/src/app/.github/node_modules
- cli_node_modules:/usr/src/app/cli/node_modules
- cli_node_modules:/usr/src/app/packages/cli/node_modules
- docs_node_modules:/usr/src/app/docs/node_modules
- e2e_node_modules:/usr/src/app/e2e/node_modules
- sdk_node_modules:/usr/src/app/packages/sdk/node_modules
@@ -74,7 +74,7 @@ services:
- ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store
- ../plugins:/build/corePlugin
- ../packages/plugin-core:/build/plugins/immich-plugin-core
env_file:
- .env
environment:
@@ -157,7 +157,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
+1 -1
View File
@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
restart: always
+1 -1
View File
@@ -61,7 +61,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
user: '1000:1000'
security_opt:
- no-new-privileges:true
+1 -1
View File
@@ -49,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
restart: always
+5 -46
View File
@@ -243,8 +243,8 @@ To connect the mobile app to your Dev Container:
- **Server code** (`/server`): Changes trigger automatic restart
- **Web code** (`/web`): Changes trigger hot module replacement
- **Database migrations**: Run `pnpm run sync:sql` in the server directory
- **API changes**: Regenerate TypeScript SDK with `make open-api`
- **Database migrations**: Run `mise //:sql`
- **API changes**: Regenerate TypeScript SDK with `mise //:open-api`
## Testing
@@ -252,20 +252,11 @@ To connect the mobile app to your Dev Container:
The Dev Container supports multiple ways to run tests:
#### Using Make Commands (Recommended)
#### Using Mise Commands (Recommended)
```bash
# Run tests for specific components
make test-server # Server unit tests
make test-web # Web unit tests
make test-e2e # End-to-end tests
make test-cli # CLI tests
# Run all tests
make test-all # Runs tests for all components
# Medium tests (integration tests)
make test-medium-dev # End-to-end tests
mise run checklist # in `server/`, `web/`, `packages/cli`
```
#### Using PNPM Directly
@@ -289,48 +280,16 @@ pnpm run test # Run API tests
pnpm run test:web # Run web UI tests
```
### Code Quality Commands
```bash
# Linting
make lint-server # Lint server code
make lint-web # Lint web code
make lint-all # Lint all components
# Formatting
make format-server # Format server code
make format-web # Format web code
make format-all # Format all code
# Type checking
make check-server # Type check server
make check-web # Type check web
make check-all # Check all components
# Complete hygiene check
make hygiene-all # Run lint, format, check, SQL sync, and audit
```
### Additional Make Commands
```bash
# Build commands
make build-server # Build server
make build-web # Build web app
make build-all # Build everything
# API generation
make open-api # Generate OpenAPI specs
make open-api-typescript # Generate TypeScript SDK
make open-api-dart # Generate Dart SDK
# Database
make sql # Sync database schema
# Dependencies
make install-server # Install server dependencies
make install-web # Install web dependencies
make install-all # Install all dependencies
mise sql # Sync database schema
```
### Debugging
+2 -1
View File
@@ -10,7 +10,8 @@ Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](ht
| :------------------ | :------------------------------------------------------------------- |
| `.github/` | Github templates and action workflows |
| `.vscode/` | VSCode debug launch profiles |
| `cli/` | Source code for the work-in-progress CLI rewrite |
| `packages/cli` | Source code for the CLI |
| `packages/sdk` | Source code for the generated OpenAPI SDK |
| `docker/` | Docker compose resources for dev, test, production |
| `design/` | Screenshots and logos for the README |
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
+11 -9
View File
@@ -34,21 +34,23 @@ Run all web checks with `pnpm run check:all`
Run all server checks with `pnpm run check:all`
:::
:::info Auto Fix
:::tip Auto Fix
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
:::
## Mobile Checks
## Mobile Checklist
The following commands must be executed from within the mobile app directory of the codebase.
- [ ] `mise //mobile:codegen` (auto-generate files using build_runner)
- [ ] `mise //mobile:lint` (static analysis via Dart Analyzer and DCM)
- [ ] `mise //mobile:format` (formatting via Dart Formatter)
- [ ] `mise //mobile:test` (unit tests)
- [ ] `make build` (auto-generate files using build_runner)
- [ ] `make analyze` (static analysis via Dart Analyzer and DCM)
- [ ] `make format` (formatting via Dart Formatter)
- [ ] `make test` (unit tests)
:::tip
Run all these commands at once with `mise //mobile:checklist`
:::
:::info Auto Fix
You can use `dart fix --apply` and `dcm fix lib` to potentially correct some issues automatically for `make analyze`.
:::tip Auto Fix
You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
:::
## OpenAPI
+4 -5
View File
@@ -17,15 +17,14 @@ make e2e
Before you can run the tests, you need to run the following commands _once_:
- `pnpm install` (in `e2e/`)
- `pnpm run build` (in `cli/`)
- `make open-api` (in the project root `/`)
- `pnpm install`
- `pnpm --filter "@immich/*" build`
- `mise //:open-api`
Once the test environment is running, the e2e tests can be run via:
```bash
cd e2e/
pnpm test
mise //e2e:test
```
The tests check various things including:
+2 -2
View File
@@ -4,7 +4,7 @@ services:
e2e-auth-server:
container_name: immich-e2e-auth-server
build:
context: ../e2e-auth-server
context: ../packages/e2e-auth-server
ports:
- 2286:2286
@@ -44,7 +44,7 @@ services:
redis:
container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
@@ -7,7 +7,6 @@ import {
getMyUser,
LoginResponseDto,
SharedLinkType,
updateConfig,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
@@ -24,7 +23,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetDir = `${testAssetDir}/metadata/faces`;
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -185,78 +183,6 @@ describe('/asset', () => {
});
});
describe('faces', () => {
const metadataFaceTests = [
{
description: 'without orientation',
filename: 'portrait.jpg',
},
{
description: 'adjusting face regions to orientation',
filename: 'portrait-orientation-6.jpg',
},
];
// should produce same resulting face region coordinates for any orientation
const expectedFaces = [
{
name: 'Marie Curie',
birthDate: null,
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 261,
boundingBoxX2: 356,
boundingBoxY1: 146,
boundingBoxY2: 284,
sourceType: 'exif',
},
],
},
{
name: 'Pierre Curie',
birthDate: null,
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 536,
boundingBoxX2: 618,
boundingBoxY1: 83,
boundingBoxY2: 252,
sourceType: 'exif',
},
],
},
];
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
const config = await utils.getSystemConfig(admin.accessToken);
config.metadata.faces.import = true;
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
const facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename,
bytes: await readFile(`${facesAssetDir}/${filename}`),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
const { status, body } = await request(app)
.get(`/assets/${facesAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
expect(sortedPeople).toMatchObject(expectedFaces);
});
});
it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
+12 -1
View File
@@ -441,7 +441,18 @@ describe('/search', () => {
.get('/search/explore')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([{ fieldName: 'exifInfo.city', items: [] }]);
expect(Array.isArray(body)).toBe(true);
expect(body).toEqual(expect.arrayContaining([{ fieldName: 'exifInfo.city', items: [] }]));
expect(body).toEqual(
expect.arrayContaining([
{
fieldName: 'createdAt',
items: expect.arrayContaining([
expect.objectContaining({ data: expect.objectContaining({ id: assetLast.id }) }),
]),
},
]),
);
});
});
+1 -1
View File
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { immichCli } from 'src/utils';
import { describe, expect, it } from 'vitest';
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
const pkg = JSON.parse(readFileSync('../packages/cli/package.json', 'utf8'));
describe(`immich --version`, () => {
describe('immich --version', () => {
@@ -28,6 +28,7 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
ownerId: [],
ratio: [],
thumbhash: [],
createdAt: [],
fileCreatedAt: [],
localOffsetHours: [],
isFavorite: [],
@@ -338,7 +339,6 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
livePhotoVideoId: asset.livePhotoVideoId,
tags: [],
people: [],
unassignedFaces: [],
stack: asset.stack,
isOffline: false,
hasMetadata: true,
@@ -66,7 +66,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
livePhotoVideoId: null,
tags: [],
people: [],
unassignedFaces: [],
stack: undefined,
isOffline: false,
hasMetadata: true,
+1 -1
View File
@@ -90,7 +90,7 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../cli' }).promise;
executeCommand('pnpm', ['exec', 'immich', '-d', `/${tempDir}/immich/`, ...args], { cwd: '../packages/cli' }).promise;
export const dockerExec = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', args.join(' ')]);
export const immichAdmin = (args: string[]) => dockerExec([`immich-admin ${args.join(' ')}`]);
+7 -7
View File
@@ -22,13 +22,12 @@
"add_birthday": "Add a birthday",
"add_endpoint": "Add endpoint",
"add_exclusion_pattern": "Add exclusion pattern",
"add_filter": "Add filter",
"add_filter_description": "Click to add a filter condition",
"add_location": "Add location",
"add_more_users": "Add more users",
"add_partner": "Add partner",
"add_path": "Add path",
"add_photos": "Add photos",
"add_step": "Add step",
"add_tag": "Add tag",
"add_to": "Add to…",
"add_to_album": "Add to album",
@@ -42,7 +41,6 @@
"add_to_shared_album": "Add to shared album",
"add_upload_to_stack": "Add upload to stack",
"add_url": "Add URL",
"add_workflow_step": "Add workflow step",
"added_to_archive": "Added to archive",
"added_to_favorites": "Added to favorites",
"added_to_favorites_count": "Added {count, number} to favorites",
@@ -809,6 +807,7 @@
"comments_are_disabled": "Comments are disabled",
"common_create_new_album": "Create new album",
"completed": "Completed",
"configuration": "Configuration",
"confirm": "Confirm",
"confirm_admin_password": "Confirm Admin Password",
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
@@ -1629,7 +1628,6 @@
"next": "Next",
"next_memory": "Next memory",
"no": "No",
"no_actions_added": "No actions added yet",
"no_albums_found": "No albums found",
"no_albums_message": "Create an album to organize your photos and videos",
"no_albums_with_name_yet": "It looks like you do not have any albums with this name yet.",
@@ -1646,7 +1644,6 @@
"no_exif_info_available": "No exif info available",
"no_explore_results_message": "Upload more photos to explore your collection.",
"no_favorites_message": "Add favorites to quickly find your best pictures and videos",
"no_filters_added": "No filters added yet",
"no_libraries_message": "Create an external library to view your photos and videos",
"no_local_assets_found": "No local assets found with this checksum",
"no_location_set": "No location set",
@@ -1659,6 +1656,7 @@
"no_results": "No results",
"no_results_description": "Try a synonym or more general keyword",
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
"no_steps": "No steps added yet",
"no_uploads_in_progress": "No uploads in progress",
"none": "None",
"not_allowed": "Not allowed",
@@ -1893,6 +1891,7 @@
"remove_assets_title": "Remove assets?",
"remove_custom_date_range": "Remove custom date range",
"remove_deleted_assets": "Remove Deleted Assets",
"remove_filter": "Remove filter",
"remove_from_album": "Remove from album",
"remove_from_album_action_prompt": "{count} removed from the album",
"remove_from_favorites": "Remove from favorites",
@@ -2236,6 +2235,7 @@
"start_date_before_end_date": "Start date must be before end date",
"state": "State",
"status": "Status",
"steps": "Steps",
"stop_casting": "Stop casting",
"stop_motion_photo": "Stop Motion Photo",
"stop_photo_sharing": "Stop sharing your photos?",
@@ -2329,7 +2329,7 @@
"trash_page_title": "Trash ({count})",
"trashed_items_will_be_permanently_deleted_after": "Trashed items will be permanently deleted after {days, plural, one {# day} other {# days}}.",
"trigger": "Trigger",
"trigger_asset_uploaded": "Asset Uploaded",
"trigger_asset_uploaded": "Asset Upload",
"trigger_asset_uploaded_description": "Triggered when a new asset is uploaded",
"trigger_description": "An event that kicks off the workflow",
"trigger_person_recognized": "Person Recognized",
@@ -2369,7 +2369,6 @@
"unsupported_field_type": "Unsupported field type",
"unsupported_file_type": "File {file} can't be uploaded because its file type {type} is not supported.",
"untagged": "Untagged",
"untitled_workflow": "Untitled workflow",
"up_next": "Up next",
"update_location_action_prompt": "Update the location of {count} selected assets with:",
"updated_at": "Updated",
@@ -2461,6 +2460,7 @@
"welcome_to_immich": "Welcome to Immich",
"width": "Width",
"wifi_name": "Wi-Fi Name",
"workflow": "Workflow",
"workflow_delete_prompt": "Are you sure you want to delete this workflow?",
"workflow_deleted": "Workflow deleted",
"workflow_description": "Workflow description",
-13
View File
@@ -1,13 +0,0 @@
{
"name": "immich-i18n",
"version": "2.7.5",
"private": true,
"scripts": {
"format": "prettier --cache --check .",
"format:fix": "prettier --cache --write --list-different ."
},
"devDependencies": {
"prettier": "^3.7.4",
"prettier-plugin-sort-json": "^4.1.1"
}
}
+2 -5
View File
@@ -64,16 +64,13 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
pnpm version "$NEXT_SERVER" --no-git-tag-version
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix i18n
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
# copy version to open-api spec
pnpm install --frozen-lockfile --prefix server
pnpm --prefix server run build
( cd ./open-api && bash ./bin/generate-open-api.sh )
mise run //:open-api
uv version --directory machine-learning "$NEXT_SERVER"
+38 -8
View File
@@ -2,9 +2,9 @@ experimental_monorepo_root = true
[monorepo]
config_roots = [
"plugins",
"packages/plugin-core",
"server",
"cli",
"packages/cli",
"deployment",
"mobile",
"e2e",
@@ -21,6 +21,7 @@ pnpm = "10.33.1"
terragrunt = "1.0.3"
opentofu = "1.11.6"
java = "21.0.2"
"npm:oazapfts" = "7.5.0"
[tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0"
@@ -40,20 +41,49 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
experimental = true
pin = true
[tasks.plugins]
run = [
"pnpm install --frozen-lockfile",
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build"
]
[tasks.open-api-typescript]
run = [
"oazapfts --optimistic --argumentStyle=object --useEnumType --allSchemas open-api/immich-openapi-specs.json packages/sdk/src/fetch-client.ts",
{ task = "//:sdk:install" },
{ task = "//:sdk:build" },
]
[tasks.open-api-dart]
dir = "open-api"
run = "bash ./bin/generate-dart-sdk.sh"
[tasks.open-api]
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
run = [
{ task = "//server:install" },
{ task = "//server:build" },
{ task = "//server:sync-open-api" },
{ task = ":open-api-typescript"},
{ task = ":open-api-dart"},
]
[tasks.sql]
dir = "server"
run = "node ./dist/bin/sync-sql.js"
# SDK tasks
[tasks."sdk:install"]
dir = "packages/sdk"
run = "pnpm install --filter @immich/sdk --frozen-lockfile"
run = "pnpm --filter @immich/sdk install --frozen-lockfile"
[tasks."sdk:build"]
dir = "packages/sdk"
run = "pnpm run build"
run = "pnpm build"
# i18n tasks
[tasks."i18n:format"]
dir = "i18n"
run = "pnpm run format"
run = "pnpm format"
[tasks."i18n:format-fix"]
dir = "i18n"
run = "pnpm run format:fix"
run = "pnpm format:fix"
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
}
}
}
fun onAndroidUpload(callback: (Result<Unit>) -> Unit)
fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) {
channel.send(listOf(maxMinutesArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) :
* This method acts as a bridge between the native Android background task system and Flutter.
*/
override fun onInitialized() {
flutterApi?.onAndroidUpload { handleHostResult(it) }
flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
}
// TODO: Move this to a separate NotificationManager class
+117 -117
View File
@@ -1003,20 +1003,6 @@
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
"unique": false,
"columns": []
}
},
{
"id": 12,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "UQ_remote_assets_owner_checksum",
@@ -1026,7 +1012,7 @@
}
},
{
"id": 13,
"id": 12,
"references": [
1
],
@@ -1040,7 +1026,7 @@
}
},
{
"id": 14,
"id": 13,
"references": [
1
],
@@ -1054,7 +1040,7 @@
}
},
{
"id": 15,
"id": 14,
"references": [
1
],
@@ -1067,36 +1053,22 @@
"columns": []
}
},
{
"id": 15,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created\nON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)\n",
"unique": false,
"columns": []
}
},
{
"id": 16,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_day",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 17,
"references": [
1
],
"type": "index",
"data": {
"on": 1,
"name": "idx_remote_asset_local_date_time_month",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
"unique": false,
"columns": []
}
},
{
"id": 18,
"references": [],
"type": "table",
"data": {
@@ -1226,7 +1198,7 @@
}
},
{
"id": 19,
"id": 17,
"references": [
0
],
@@ -1301,7 +1273,7 @@
}
},
{
"id": 20,
"id": 18,
"references": [
0
],
@@ -1388,7 +1360,7 @@
}
},
{
"id": 21,
"id": 19,
"references": [
1
],
@@ -1644,7 +1616,7 @@
}
},
{
"id": 22,
"id": 20,
"references": [
1,
4
@@ -1718,7 +1690,7 @@
}
},
{
"id": 23,
"id": 21,
"references": [
4,
0
@@ -1806,7 +1778,7 @@
}
},
{
"id": 24,
"id": 22,
"references": [
1
],
@@ -1902,7 +1874,7 @@
}
},
{
"id": 25,
"id": 23,
"references": [
0
],
@@ -2066,10 +2038,10 @@
}
},
{
"id": 26,
"id": 24,
"references": [
1,
25
23
],
"type": "table",
"data": {
@@ -2140,7 +2112,7 @@
}
},
{
"id": 27,
"id": 25,
"references": [
0
],
@@ -2284,10 +2256,10 @@
}
},
{
"id": 28,
"id": 26,
"references": [
1,
27
25
],
"type": "table",
"data": {
@@ -2461,7 +2433,7 @@
}
},
{
"id": 29,
"id": 27,
"references": [],
"type": "table",
"data": {
@@ -2509,7 +2481,7 @@
}
},
{
"id": 30,
"id": 28,
"references": [],
"type": "table",
"data": {
@@ -2684,7 +2656,7 @@
}
},
{
"id": 31,
"id": 29,
"references": [
1
],
@@ -2778,7 +2750,7 @@
}
},
{
"id": 32,
"id": 30,
"references": [],
"type": "table",
"data": {
@@ -2826,13 +2798,13 @@
}
},
{
"id": 33,
"id": 31,
"references": [
20
18
],
"type": "index",
"data": {
"on": 20,
"on": 18,
"name": "idx_partner_shared_with_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
"unique": false,
@@ -2840,19 +2812,47 @@
}
},
{
"id": 34,
"id": 32,
"references": [
21
19
],
"type": "index",
"data": {
"on": 21,
"on": 19,
"name": "idx_lat_lng",
"sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
"unique": false,
"columns": []
}
},
{
"id": 33,
"references": [
19
],
"type": "index",
"data": {
"on": 19,
"name": "idx_remote_exif_city",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city\nON remote_exif_entity (city) WHERE city IS NOT NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 34,
"references": [
20
],
"type": "index",
"data": {
"on": 20,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{
"id": 35,
"references": [
@@ -2861,20 +2861,6 @@
"type": "index",
"data": {
"on": 22,
"name": "idx_remote_album_asset_album_asset",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
"unique": false,
"columns": []
}
},
{
"id": 36,
"references": [
24
],
"type": "index",
"data": {
"on": 24,
"name": "idx_remote_asset_cloud_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
"unique": false,
@@ -2882,13 +2868,13 @@
}
},
{
"id": 37,
"id": 36,
"references": [
27
25
],
"type": "index",
"data": {
"on": 27,
"on": 25,
"name": "idx_person_owner_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
"unique": false,
@@ -2896,13 +2882,13 @@
}
},
{
"id": 38,
"id": 37,
"references": [
28
26
],
"type": "index",
"data": {
"on": 28,
"on": 26,
"name": "idx_asset_face_person_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
"unique": false,
@@ -2910,13 +2896,13 @@
}
},
{
"id": 39,
"id": 38,
"references": [
28
26
],
"type": "index",
"data": {
"on": 28,
"on": 26,
"name": "idx_asset_face_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
"unique": false,
@@ -2924,13 +2910,27 @@
}
},
{
"id": 40,
"id": 39,
"references": [
30
26
],
"type": "index",
"data": {
"on": 30,
"on": 26,
"name": "idx_asset_face_visible_person",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person\nON asset_face_entity (person_id, asset_id)\nWHERE is_visible = 1 AND deleted_at IS NULL\n",
"unique": false,
"columns": []
}
},
{
"id": 40,
"references": [
28
],
"type": "index",
"data": {
"on": 28,
"name": "idx_trashed_local_asset_checksum",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
"unique": false,
@@ -2940,11 +2940,11 @@
{
"id": 41,
"references": [
30
28
],
"type": "index",
"data": {
"on": 30,
"on": 28,
"name": "idx_trashed_local_asset_album",
"sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
"unique": false,
@@ -2954,11 +2954,11 @@
{
"id": 42,
"references": [
31
29
],
"type": "index",
"data": {
"on": 31,
"on": 29,
"name": "idx_asset_edit_asset_id",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
"unique": false,
@@ -3066,15 +3066,6 @@
}
]
},
{
"name": "idx_remote_asset_owner_checksum",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
}
]
},
{
"name": "UQ_remote_assets_owner_checksum",
"sql": [
@@ -3112,20 +3103,11 @@
]
},
{
"name": "idx_remote_asset_local_date_time_day",
"name": "idx_remote_asset_owner_visibility_deleted_created",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))"
}
]
},
{
"name": "idx_remote_asset_local_date_time_month",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)"
}
]
},
@@ -3282,6 +3264,15 @@
}
]
},
{
"name": "idx_remote_exif_city",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL"
}
]
},
{
"name": "idx_remote_album_asset_album_asset",
"sql": [
@@ -3327,6 +3318,15 @@
}
]
},
{
"name": "idx_asset_face_visible_person",
"sql": [
{
"dialect": "sqlite",
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL"
}
]
},
{
"name": "idx_trashed_local_asset_checksum",
"sql": [
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol {
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
}
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
}
}
}
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) {
func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
channel.sendMessage(nil) { response in
channel.sendMessage([maxMinutesArg] as [Any?]) { response in
guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName)))
return
@@ -10,6 +10,7 @@ class RemoteAsset extends BaseAsset {
final AssetVisibility visibility;
final String ownerId;
final String? stackId;
final DateTime? uploadedAt;
const RemoteAsset({
required this.id,
@@ -20,6 +21,7 @@ class RemoteAsset extends BaseAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
this.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -55,6 +57,7 @@ class RemoteAsset extends BaseAsset {
type: $type,
createdAt: $createdAt,
updatedAt: $updatedAt,
uploadedAt: ${uploadedAt ?? "<NA>"},
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
@@ -82,7 +85,8 @@ class RemoteAsset extends BaseAsset {
ownerId == other.ownerId &&
thumbHash == other.thumbHash &&
visibility == other.visibility &&
stackId == other.stackId;
stackId == other.stackId &&
uploadedAt == other.uploadedAt;
}
@override
@@ -93,7 +97,8 @@ class RemoteAsset extends BaseAsset {
localId.hashCode ^
thumbHash.hashCode ^
visibility.hashCode ^
stackId.hashCode;
stackId.hashCode ^
uploadedAt.hashCode;
RemoteAsset copyWith({
String? id,
@@ -104,6 +109,7 @@ class RemoteAsset extends BaseAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -123,6 +129,7 @@ class RemoteAsset extends BaseAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
@@ -148,6 +155,7 @@ class RemoteAssetExif extends RemoteAsset {
required super.type,
required super.createdAt,
required super.updatedAt,
super.uploadedAt,
super.width,
super.height,
super.durationMs,
@@ -184,6 +192,7 @@ class RemoteAssetExif extends RemoteAsset {
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
DateTime? uploadedAt,
int? width,
int? height,
int? durationMs,
@@ -204,6 +213,7 @@ class RemoteAssetExif extends RemoteAsset {
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
@@ -1,22 +1,25 @@
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
class AppConfig {
final ThemeConfig theme;
final CleanupConfig cleanup;
final MapConfig map;
const AppConfig({this.theme = const .new(), this.cleanup = const .new()});
const AppConfig({this.theme = const .new(), this.cleanup = const .new(), this.map = const .new()});
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup}) =>
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup);
AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map}) =>
.new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map);
@override
bool operator ==(Object other) =>
identical(this, other) || (other is AppConfig && other.theme == theme && other.cleanup == cleanup);
identical(this, other) ||
(other is AppConfig && other.theme == theme && other.cleanup == cleanup && other.map == map);
@override
int get hashCode => Object.hash(theme, cleanup);
int get hashCode => Object.hash(theme, cleanup, map);
@override
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup)';
String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map)';
}
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
class MapConfig {
final int relativeDays;
final bool favoritesOnly;
final bool includeArchived;
final ThemeMode themeMode;
final bool withPartners;
const MapConfig({
this.relativeDays = 0,
this.favoritesOnly = false,
this.includeArchived = false,
this.themeMode = ThemeMode.system,
this.withPartners = false,
});
MapConfig copyWith({
int? relativeDays,
bool? favoritesOnly,
bool? includeArchived,
ThemeMode? themeMode,
bool? withPartners,
}) => MapConfig(
relativeDays: relativeDays ?? this.relativeDays,
favoritesOnly: favoritesOnly ?? this.favoritesOnly,
includeArchived: includeArchived ?? this.includeArchived,
themeMode: themeMode ?? this.themeMode,
withPartners: withPartners ?? this.withPartners,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MapConfig &&
other.relativeDays == relativeDays &&
other.favoritesOnly == favoritesOnly &&
other.includeArchived == includeArchived &&
other.themeMode == themeMode &&
other.withPartners == withPartners);
@override
int get hashCode => Object.hash(relativeDays, favoritesOnly, includeArchived, themeMode, withPartners);
@override
String toString() =>
'MapConfig(relativeDays: $relativeDays, favoritesOnly: $favoritesOnly, includeArchived: $includeArchived, themeMode: $themeMode, withPartners: $withPartners)';
}
@@ -26,6 +26,13 @@ enum MetadataKey<T extends Object> {
// Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
// Map
mapShowFavoriteOnly<bool>(.appConfig, 'map.showFavoriteOnly', false),
mapRelativeDate<int>(.appConfig, 'map.relativeDate', 0),
mapIncludeArchived<bool>(.appConfig, 'map.includeArchived', false),
mapThemeMode<ThemeMode>(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
mapWithPartners<bool>(.appConfig, 'map.withPartners', false),
// Cleanup
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
cleanupKeepMediaType<AssetKeepType>(
+5 -6
View File
@@ -36,15 +36,9 @@ enum StoreKey<T> {
advancedTroubleshooting<bool>._(114),
preferRemoteImage<bool>._(116),
loopVideo<bool>._(117),
// map related settings
mapShowFavoriteOnly<bool>._(118),
mapRelativeDate<int>._(119),
selfSignedCert<bool>._(120),
mapIncludeArchived<bool>._(121),
ignoreIcloudAssets<bool>._(122),
selectedAlbumSortReverse<bool>._(123),
mapThemeMode<int>._(124),
mapwithPartners<bool>._(125),
enableHapticFeedback<bool>._(126),
customHeaders<String>._(127),
@@ -93,6 +87,11 @@ enum StoreKey<T> {
legacyCleanupKeepAlbumIds<String>._(1010),
legacyCleanupCutoffDaysAgo<int>._(1011),
legacyCleanupDefaultsInitialized<bool>._(1012),
legacyMapRelativeDate<int>._(119),
legacyMapShowFavoriteOnly<bool>._(118),
legacyMapIncludeArchived<bool>._(121),
legacyMapThemeMode<int>._(124),
legacyMapwithPartners<bool>._(125),
legacyLogLevel<int>._(115);
const StoreKey._(this.id);
@@ -2,6 +2,8 @@ enum GroupAssetsBy { day, month, auto, none }
enum HeaderType { none, month, day, monthAndDay }
enum SortAssetsBy { taken, uploaded }
class Bucket {
final int assetCount;
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
}
@override
Future<void> onAndroidUpload() async {
_logger.info('Android background processing started');
final sw = Stopwatch()..start();
try {
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
await _handleBackup();
} catch (error, stack) {
_logger.severe("Failed to complete Android background processing", error, stack);
} finally {
sw.stop();
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
Future<void> onAndroidUpload(int? maxMinutes) async {
final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
return _backgroundLoop(
hashTimeout: hashTimeout,
backupTimeout: backupTimeout,
debugLabel: 'Android background upload',
);
}
@override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null;
return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload');
}
Future<void> _backgroundLoop({
required Duration hashTimeout,
required Duration? backupTimeout,
required String debugLabel,
}) async {
_logger.info(
'$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m',
);
final sw = Stopwatch()..start();
try {
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
if (!await _syncAssets(hashTimeout: timeout)) {
if (!await _syncAssets(hashTimeout: hashTimeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup");
return;
}
final backupFuture = _handleBackup();
if (maxSeconds != null) {
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
} else {
Timer? cancelTimer;
if (backupTimeout != null) {
cancelTimer = Timer(backupTimeout, () {
if (!_cancellationToken.isCompleted) {
_logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
_cancellationToken.complete();
}
});
}
try {
await backupFuture;
} finally {
cancelTimer?.cancel();
}
} catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack);
_logger.severe("Failed to complete $debugLabel", error, stack);
} finally {
sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
_logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
await _cleanup();
}
}
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_logger.info("Cleaning up background worker");
_cancellationToken.complete();
if (!_cancellationToken.isCompleted) {
_cancellationToken.complete();
}
final cleanupFutures = [
nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async {
@@ -24,6 +24,7 @@ enum SyncMigrationTask {
v20260128_ResetExifV1, // EXIF table has incorrect width and height information.
v20260128_CopyExifWidthHeightToAsset, // Asset table has incorrect width and height for video ratio calculations.
v20260128_ResetAssetV1, // Asset v2.5.0 has width and height information that were edited assets.
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
}
class SyncStreamService {
@@ -132,6 +133,13 @@ class SyncStreamService {
migrations.add(SyncMigrationTask.v20260128_CopyExifWidthHeightToAsset.name);
}
}
if (!migrations.contains(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name) &&
semVer > const SemVer(major: 2, minor: 7, patch: 5)) {
_logger.info("Running pre-sync task: v20260597_ResetAssetV1AssetV2");
await _syncApiRepository.deleteSyncAck([SyncEntityType.assetV1, SyncEntityType.assetV2]);
migrations.add(SyncMigrationTask.v20260597_ResetAssetV1AssetV2.name);
}
}
Future<void> _runPostSyncTasks(List<String> migrations) async {
@@ -35,6 +35,7 @@ enum TimelineOrigin {
deepLink,
albumActivities,
folder,
recentlyAdded,
}
class TimelineFactory {
@@ -61,6 +62,8 @@ class TimelineFactory {
TimelineService remoteAssets(String userId) => TimelineService(_timelineRepository.remote(userId, groupBy));
TimelineService recentlyAdded(String userId) => TimelineService(_timelineRepository.recentlyAdded(userId, groupBy));
TimelineService favorite(String userId) => TimelineService(_timelineRepository.favorite(userId, groupBy));
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
@@ -11,6 +11,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
@@ -33,6 +34,7 @@ extension DTOToAsset on api.AssetResponseDto {
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
uploadedAt: createdAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration,
@@ -5,6 +5,11 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person
ON asset_face_entity (person_id, asset_id)
WHERE is_visible = 1 AND deleted_at IS NULL
''')
class AssetFaceEntity extends Table with DriftDefaultsMixin {
const AssetFaceEntity();
@@ -1350,3 +1350,7 @@ i0.Index get idxAssetFaceAssetId => i0.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
i0.Index get idxAssetFaceVisiblePerson => i0.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
@@ -6,6 +6,10 @@ import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)')
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_exif_city
ON remote_exif_entity (city) WHERE city IS NOT NULL
''')
class RemoteExifEntity extends Table with DriftDefaultsMixin {
const RemoteExifEntity();
@@ -1883,3 +1883,8 @@ class RemoteExifEntityCompanion
.toString();
}
}
i0.Index get idxRemoteExifCity => i0.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
@@ -4,7 +4,7 @@ import 'local_asset.entity.dart';
import 'local_album.entity.dart';
import 'local_album_asset.entity.dart';
mergedAsset:
mergedAsset:
SELECT
rae.id as remote_id,
(SELECT lae.id FROM local_asset_entity lae WHERE lae.checksum = rae.checksum LIMIT 1) as local_id,
@@ -27,7 +27,8 @@ SELECT
NULL as longitude,
NULL as adjustmentTime,
rae.is_edited,
0 as playback_style
0 as playback_style,
rae.uploaded_at
FROM
remote_asset_entity rae
LEFT JOIN
@@ -65,7 +66,8 @@ SELECT
lae.longitude,
lae.adjustment_time,
0 as is_edited,
lae.playback_style
lae.playback_style,
NULL as uploaded_at
FROM
local_asset_entity lae
WHERE NOT EXISTS (
+4 -1
View File
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
);
$arrayStartIndex += generatedlimit.amountOfVariables;
return customSelect(
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_ms, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style, rae.uploaded_at FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_ms, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style, NULL AS uploaded_at FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
variables: [
for (var $ in userIds) i0.Variable<String>($),
...generatedlimit.introducedVariables,
@@ -68,6 +68,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
isEdited: row.read<bool>('is_edited'),
playbackStyle: row.read<int>('playback_style'),
uploadedAt: row.readNullable<DateTime>('uploaded_at'),
),
);
}
@@ -141,6 +142,7 @@ class MergedAssetResult {
final DateTime? adjustmentTime;
final bool isEdited;
final int playbackStyle;
final DateTime? uploadedAt;
MergedAssetResult({
this.remoteId,
this.localId,
@@ -164,6 +166,7 @@ class MergedAssetResult {
this.adjustmentTime,
required this.isEdited,
required this.playbackStyle,
this.uploadedAt,
});
}
@@ -5,9 +5,6 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
@TableIndex.sql(
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
)
@TableIndex.sql('''
CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum
ON remote_asset_entity (owner_id, checksum)
@@ -20,12 +17,10 @@ WHERE (library_id IS NOT NULL);
''')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)')
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)')
@TableIndex.sql(
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
)
@TableIndex.sql(
"CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
)
@TableIndex.sql('''
CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created
ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)
''')
class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
const RemoteAssetEntity();
@@ -43,6 +38,8 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
DateTimeColumn get deletedAt => dateTime().nullable()();
DateTimeColumn get uploadedAt => dateTime().nullable()();
TextColumn get livePhotoVideoId => text().nullable()();
IntColumn get visibility => intEnum<AssetVisibility>()();
@@ -66,6 +63,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
uploadedAt: uploadedAt,
durationMs: durationMs,
isFavorite: isFavorite,
height: height,
@@ -27,6 +27,7 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder =
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<DateTime?> uploadedAt,
i0.Value<String?> livePhotoVideoId,
required i2.AssetVisibility visibility,
i0.Value<String?> stackId,
@@ -49,6 +50,7 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder =
i0.Value<DateTime?> localDateTime,
i0.Value<String?> thumbHash,
i0.Value<DateTime?> deletedAt,
i0.Value<DateTime?> uploadedAt,
i0.Value<String?> livePhotoVideoId,
i0.Value<i2.AssetVisibility> visibility,
i0.Value<String?> stackId,
@@ -177,6 +179,11 @@ class $$RemoteAssetEntityTableFilterComposer
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnFilters(column),
@@ -305,6 +312,11 @@ class $$RemoteAssetEntityTableOrderingComposer
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => i0.ColumnOrderings(column),
@@ -412,6 +424,11 @@ class $$RemoteAssetEntityTableAnnotationComposer
i0.GeneratedColumn<DateTime> get deletedAt =>
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
i0.GeneratedColumn<DateTime> get uploadedAt => $composableBuilder(
column: $table.uploadedAt,
builder: (column) => column,
);
i0.GeneratedColumn<String> get livePhotoVideoId => $composableBuilder(
column: $table.livePhotoVideoId,
builder: (column) => column,
@@ -507,6 +524,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i0.Value<i2.AssetVisibility> visibility =
const i0.Value.absent(),
@@ -528,6 +546,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -550,6 +569,7 @@ class $$RemoteAssetEntityTableTableManager
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
@@ -570,6 +590,7 @@ class $$RemoteAssetEntityTableTableManager
localDateTime: localDateTime,
thumbHash: thumbHash,
deletedAt: deletedAt,
uploadedAt: uploadedAt,
livePhotoVideoId: livePhotoVideoId,
visibility: visibility,
stackId: stackId,
@@ -645,9 +666,9 @@ typedef $$RemoteAssetEntityTableProcessedTableManager =
i1.RemoteAssetEntityData,
i0.PrefetchHooks Function({bool ownerId})
>;
i0.Index get idxRemoteAssetOwnerChecksum => i0.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
@@ -818,6 +839,18 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _uploadedAtMeta = const i0.VerificationMeta(
'uploadedAt',
);
@override
late final i0.GeneratedColumn<DateTime> uploadedAt =
i0.GeneratedColumn<DateTime>(
'uploaded_at',
aliasedName,
true,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _livePhotoVideoIdMeta =
const i0.VerificationMeta('livePhotoVideoId');
@override
@@ -894,6 +927,7 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
localDateTime,
thumbHash,
deletedAt,
uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -998,6 +1032,12 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta),
);
}
if (data.containsKey('uploaded_at')) {
context.handle(
_uploadedAtMeta,
uploadedAt.isAcceptableOrUnknown(data['uploaded_at']!, _uploadedAtMeta),
);
}
if (data.containsKey('live_photo_video_id')) {
context.handle(
_livePhotoVideoIdMeta,
@@ -1095,6 +1135,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity
i0.DriftSqlType.dateTime,
data['${effectivePrefix}deleted_at'],
),
uploadedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}uploaded_at'],
),
livePhotoVideoId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}live_photo_video_id'],
@@ -1153,6 +1197,7 @@ class RemoteAssetEntityData extends i0.DataClass
final DateTime? localDateTime;
final String? thumbHash;
final DateTime? deletedAt;
final DateTime? uploadedAt;
final String? livePhotoVideoId;
final i2.AssetVisibility visibility;
final String? stackId;
@@ -1173,6 +1218,7 @@ class RemoteAssetEntityData extends i0.DataClass
this.localDateTime,
this.thumbHash,
this.deletedAt,
this.uploadedAt,
this.livePhotoVideoId,
required this.visibility,
this.stackId,
@@ -1212,6 +1258,9 @@ class RemoteAssetEntityData extends i0.DataClass
if (!nullToAbsent || deletedAt != null) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
}
if (!nullToAbsent || uploadedAt != null) {
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt);
}
if (!nullToAbsent || livePhotoVideoId != null) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId);
}
@@ -1252,6 +1301,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime: serializer.fromJson<DateTime?>(json['localDateTime']),
thumbHash: serializer.fromJson<String?>(json['thumbHash']),
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
uploadedAt: serializer.fromJson<DateTime?>(json['uploadedAt']),
livePhotoVideoId: serializer.fromJson<String?>(json['livePhotoVideoId']),
visibility: i1.$RemoteAssetEntityTable.$convertervisibility.fromJson(
serializer.fromJson<int>(json['visibility']),
@@ -1281,6 +1331,7 @@ class RemoteAssetEntityData extends i0.DataClass
'localDateTime': serializer.toJson<DateTime?>(localDateTime),
'thumbHash': serializer.toJson<String?>(thumbHash),
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
'uploadedAt': serializer.toJson<DateTime?>(uploadedAt),
'livePhotoVideoId': serializer.toJson<String?>(livePhotoVideoId),
'visibility': serializer.toJson<int>(
i1.$RemoteAssetEntityTable.$convertervisibility.toJson(visibility),
@@ -1306,6 +1357,7 @@ class RemoteAssetEntityData extends i0.DataClass
i0.Value<DateTime?> localDateTime = const i0.Value.absent(),
i0.Value<String?> thumbHash = const i0.Value.absent(),
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
i0.Value<DateTime?> uploadedAt = const i0.Value.absent(),
i0.Value<String?> livePhotoVideoId = const i0.Value.absent(),
i2.AssetVisibility? visibility,
i0.Value<String?> stackId = const i0.Value.absent(),
@@ -1328,6 +1380,7 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: thumbHash.present ? thumbHash.value : this.thumbHash,
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
uploadedAt: uploadedAt.present ? uploadedAt.value : this.uploadedAt,
livePhotoVideoId: livePhotoVideoId.present
? livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1358,6 +1411,9 @@ class RemoteAssetEntityData extends i0.DataClass
: this.localDateTime,
thumbHash: data.thumbHash.present ? data.thumbHash.value : this.thumbHash,
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
uploadedAt: data.uploadedAt.present
? data.uploadedAt.value
: this.uploadedAt,
livePhotoVideoId: data.livePhotoVideoId.present
? data.livePhotoVideoId.value
: this.livePhotoVideoId,
@@ -1387,6 +1443,7 @@ class RemoteAssetEntityData extends i0.DataClass
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('uploadedAt: $uploadedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
@@ -1412,6 +1469,7 @@ class RemoteAssetEntityData extends i0.DataClass
localDateTime,
thumbHash,
deletedAt,
uploadedAt,
livePhotoVideoId,
visibility,
stackId,
@@ -1436,6 +1494,7 @@ class RemoteAssetEntityData extends i0.DataClass
other.localDateTime == this.localDateTime &&
other.thumbHash == this.thumbHash &&
other.deletedAt == this.deletedAt &&
other.uploadedAt == this.uploadedAt &&
other.livePhotoVideoId == this.livePhotoVideoId &&
other.visibility == this.visibility &&
other.stackId == this.stackId &&
@@ -1459,6 +1518,7 @@ class RemoteAssetEntityCompanion
final i0.Value<DateTime?> localDateTime;
final i0.Value<String?> thumbHash;
final i0.Value<DateTime?> deletedAt;
final i0.Value<DateTime?> uploadedAt;
final i0.Value<String?> livePhotoVideoId;
final i0.Value<i2.AssetVisibility> visibility;
final i0.Value<String?> stackId;
@@ -1479,6 +1539,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
this.visibility = const i0.Value.absent(),
this.stackId = const i0.Value.absent(),
@@ -1500,6 +1561,7 @@ class RemoteAssetEntityCompanion
this.localDateTime = const i0.Value.absent(),
this.thumbHash = const i0.Value.absent(),
this.deletedAt = const i0.Value.absent(),
this.uploadedAt = const i0.Value.absent(),
this.livePhotoVideoId = const i0.Value.absent(),
required i2.AssetVisibility visibility,
this.stackId = const i0.Value.absent(),
@@ -1526,6 +1588,7 @@ class RemoteAssetEntityCompanion
i0.Expression<DateTime>? localDateTime,
i0.Expression<String>? thumbHash,
i0.Expression<DateTime>? deletedAt,
i0.Expression<DateTime>? uploadedAt,
i0.Expression<String>? livePhotoVideoId,
i0.Expression<int>? visibility,
i0.Expression<String>? stackId,
@@ -1547,6 +1610,7 @@ class RemoteAssetEntityCompanion
if (localDateTime != null) 'local_date_time': localDateTime,
if (thumbHash != null) 'thumb_hash': thumbHash,
if (deletedAt != null) 'deleted_at': deletedAt,
if (uploadedAt != null) 'uploaded_at': uploadedAt,
if (livePhotoVideoId != null) 'live_photo_video_id': livePhotoVideoId,
if (visibility != null) 'visibility': visibility,
if (stackId != null) 'stack_id': stackId,
@@ -1570,6 +1634,7 @@ class RemoteAssetEntityCompanion
i0.Value<DateTime?>? localDateTime,
i0.Value<String?>? thumbHash,
i0.Value<DateTime?>? deletedAt,
i0.Value<DateTime?>? uploadedAt,
i0.Value<String?>? livePhotoVideoId,
i0.Value<i2.AssetVisibility>? visibility,
i0.Value<String?>? stackId,
@@ -1591,6 +1656,7 @@ class RemoteAssetEntityCompanion
localDateTime: localDateTime ?? this.localDateTime,
thumbHash: thumbHash ?? this.thumbHash,
deletedAt: deletedAt ?? this.deletedAt,
uploadedAt: uploadedAt ?? this.uploadedAt,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
visibility: visibility ?? this.visibility,
stackId: stackId ?? this.stackId,
@@ -1646,6 +1712,9 @@ class RemoteAssetEntityCompanion
if (deletedAt.present) {
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
}
if (uploadedAt.present) {
map['uploaded_at'] = i0.Variable<DateTime>(uploadedAt.value);
}
if (livePhotoVideoId.present) {
map['live_photo_video_id'] = i0.Variable<String>(livePhotoVideoId.value);
}
@@ -1683,6 +1752,7 @@ class RemoteAssetEntityCompanion
..write('localDateTime: $localDateTime, ')
..write('thumbHash: $thumbHash, ')
..write('deletedAt: $deletedAt, ')
..write('uploadedAt: $uploadedAt, ')
..write('livePhotoVideoId: $livePhotoVideoId, ')
..write('visibility: $visibility, ')
..write('stackId: $stackId, ')
@@ -1693,10 +1763,6 @@ class RemoteAssetEntityCompanion
}
}
i0.Index get uQRemoteAssetsOwnerChecksum => i0.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
i0.Index get uQRemoteAssetsOwnerLibraryChecksum => i0.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
@@ -1709,11 +1775,7 @@ i0.Index get idxRemoteAssetStackId => i0.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
i0.Index get idxRemoteAssetLocalDateTimeDay => i0.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
i0.Index get idxRemoteAssetLocalDateTimeMonth => i0.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
i0.Index get idxRemoteAssetOwnerVisibilityDeletedCreated => i0.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
@@ -98,7 +98,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 25;
int get schemaVersion => 26;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -266,6 +266,15 @@ class Drift extends $Drift {
},
from24To25: (m, v25) async {
await m.createTable(v25.metadata);
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_owner_checksum');
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_day');
await customStatement('DROP INDEX IF EXISTS idx_remote_asset_local_date_time_month');
await m.createIndex(v25.idxRemoteAssetOwnerVisibilityDeletedCreated);
await m.createIndex(v25.idxRemoteExifCity);
await m.createIndex(v25.idxAssetFaceVisiblePerson);
},
from25To26: (m, v26) async {
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
},
),
);
@@ -113,13 +113,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
i4.idxLocalAssetChecksum,
i4.idxLocalAssetCloudId,
i3.idxStackPrimaryAssetId,
i2.idxRemoteAssetOwnerChecksum,
i2.uQRemoteAssetsOwnerChecksum,
i2.uQRemoteAssetsOwnerLibraryChecksum,
i2.idxRemoteAssetChecksum,
i2.idxRemoteAssetStackId,
i2.idxRemoteAssetLocalDateTimeDay,
i2.idxRemoteAssetLocalDateTimeMonth,
i2.idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
@@ -137,11 +135,13 @@ abstract class $Drift extends i0.GeneratedDatabase {
metadataEntity,
i10.idxPartnerSharedWithId,
i11.idxLatLng,
i11.idxRemoteExifCity,
i12.idxRemoteAlbumAssetAlbumAsset,
i14.idxRemoteAssetCloudId,
i17.idxPersonOwnerId,
i18.idxAssetFacePersonId,
i18.idxAssetFaceAssetId,
i18.idxAssetFaceVisiblePerson,
i20.idxTrashedLocalAssetChecksum,
i20.idxTrashedLocalAssetAlbum,
i21.idxAssetEditAssetId,
+618 -14
View File
@@ -12390,13 +12390,11 @@ final class Schema25 extends i0.VersionedSchema {
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetLocalDateTimeDay,
idxRemoteAssetLocalDateTimeMonth,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
@@ -12414,11 +12412,13 @@ final class Schema25 extends i0.VersionedSchema {
metadata,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
@@ -12583,10 +12583,6 @@ final class Schema25 extends i0.VersionedSchema {
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
@@ -12603,13 +12599,9 @@ final class Schema25 extends i0.VersionedSchema {
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
'idx_remote_asset_local_date_time_day',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
);
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
'idx_remote_asset_local_date_time_month',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
@@ -12883,6 +12875,10 @@ final class Schema25 extends i0.VersionedSchema {
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
@@ -12903,6 +12899,10 @@ final class Schema25 extends i0.VersionedSchema {
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
@@ -12943,6 +12943,602 @@ i1.GeneratedColumn<String> _column_211(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL',
);
final class Schema26 extends i0.VersionedSchema {
Schema26({required super.database}) : super(version: 26);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAlbumAssetAlbumAsset,
idxLocalAssetChecksum,
idxLocalAssetCloudId,
idxStackPrimaryAssetId,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
idxRemoteAssetStackId,
idxRemoteAssetOwnerVisibilityDeletedCreated,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
remoteAssetCloudIdEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
metadata,
idxPartnerSharedWithId,
idxLatLng,
idxRemoteExifCity,
idxRemoteAlbumAssetAlbumAsset,
idxRemoteAssetCloudId,
idxPersonOwnerId,
idxAssetFacePersonId,
idxAssetFaceAssetId,
idxAssetFaceVisiblePerson,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
idxAssetEditAssetId,
];
late final Shape33 userEntity = Shape33(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 remoteAssetEntity = Shape50(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_212,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 stackEntity = Shape35(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_130,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape36 localAssetEntity = Shape36(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_131,
_column_120,
_column_132,
_column_133,
_column_134,
_column_135,
_column_136,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 remoteAlbumEntity = Shape48(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_138,
_column_114,
_column_115,
_column_139,
_column_140,
_column_141,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 localAlbumEntity = Shape38(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_115,
_column_142,
_column_143,
_column_144,
_column_145,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape39 localAlbumAssetEntity = Shape39(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_146, _column_147, _column_145],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
'idx_local_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxLocalAssetCloudId = i1.Index(
'idx_local_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
);
final i1.Index idxStackPrimaryAssetId = i1.Index(
'idx_stack_primary_asset_id',
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetStackId = i1.Index(
'idx_remote_asset_stack_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
);
final i1.Index idxRemoteAssetOwnerVisibilityDeletedCreated = i1.Index(
'idx_remote_asset_owner_visibility_deleted_created',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_visibility_deleted_created ON remote_asset_entity (owner_id, visibility, deleted_at, created_at DESC)',
);
late final Shape40 authUserEntity = Shape40(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_108,
_column_109,
_column_148,
_column_110,
_column_111,
_column_149,
_column_150,
_column_151,
_column_152,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_153, _column_154, _column_155],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 partnerEntity = Shape41(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_156, _column_157, _column_158],
attachedDatabase: database,
),
alias: null,
);
late final Shape42 remoteExifEntity = Shape42(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_160,
_column_161,
_column_162,
_column_163,
_column_164,
_column_117,
_column_116,
_column_165,
_column_166,
_column_167,
_column_168,
_column_135,
_column_136,
_column_169,
_column_170,
_column_171,
_column_172,
_column_173,
_column_174,
_column_175,
_column_176,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_159, _column_177],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_177, _column_153, _column_178],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 remoteAssetCloudIdEntity = Shape43(
source: i0.VersionedTable(
entityName: 'remote_asset_cloud_id_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_159,
_column_179,
_column_180,
_column_134,
_column_135,
_column_136,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 memoryEntity = Shape44(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_124,
_column_121,
_column_113,
_column_181,
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_159, _column_187],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 personEntity = Shape45(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_114,
_column_115,
_column_121,
_column_108,
_column_188,
_column_189,
_column_190,
_column_191,
_column_192,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 assetFaceEntity = Shape46(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_193,
_column_194,
_column_195,
_column_196,
_column_197,
_column_198,
_column_199,
_column_200,
_column_201,
_column_124,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_202, _column_203, _column_204],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 trashedLocalAssetEntity = Shape47(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_108,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_107,
_column_205,
_column_131,
_column_120,
_column_132,
_column_206,
_column_137,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 assetEditEntity = Shape32(
source: i0.VersionedTable(
entityName: 'asset_edit_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_107,
_column_159,
_column_207,
_column_208,
_column_209,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape49 metadata = Shape49(
source: i0.VersionedTable(
entityName: 'metadata',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY("key")'],
columns: [_column_210, _column_211, _column_115],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxPartnerSharedWithId = i1.Index(
'idx_partner_shared_with_id',
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxRemoteExifCity = i1.Index(
'idx_remote_exif_city',
'CREATE INDEX IF NOT EXISTS idx_remote_exif_city ON remote_exif_entity (city) WHERE city IS NOT NULL',
);
final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
'idx_remote_album_asset_album_asset',
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
);
final i1.Index idxRemoteAssetCloudId = i1.Index(
'idx_remote_asset_cloud_id',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
);
final i1.Index idxPersonOwnerId = i1.Index(
'idx_person_owner_id',
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
);
final i1.Index idxAssetFacePersonId = i1.Index(
'idx_asset_face_person_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
);
final i1.Index idxAssetFaceAssetId = i1.Index(
'idx_asset_face_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
);
final i1.Index idxAssetFaceVisiblePerson = i1.Index(
'idx_asset_face_visible_person',
'CREATE INDEX IF NOT EXISTS idx_asset_face_visible_person ON asset_face_entity (person_id, asset_id) WHERE is_visible = 1 AND deleted_at IS NULL',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
final i1.Index idxAssetEditAssetId = i1.Index(
'idx_asset_edit_asset_id',
'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
);
}
class Shape50 extends i0.VersionedTable {
Shape50({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationMs =>
columnsByName['duration_ms']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get ownerId =>
columnsByName['owner_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get localDateTime =>
columnsByName['local_date_time']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get thumbHash =>
columnsByName['thumb_hash']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get deletedAt =>
columnsByName['deleted_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uploadedAt =>
columnsByName['uploaded_at']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get livePhotoVideoId =>
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get visibility =>
columnsByName['visibility']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get stackId =>
columnsByName['stack_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get libraryId =>
columnsByName['library_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get isEdited =>
columnsByName['is_edited']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_212(String aliasedName) =>
i1.GeneratedColumn<String>(
'uploaded_at',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -12968,6 +13564,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -13091,6 +13688,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from24To25(migrator, schema);
return 25;
case 25:
final schema = Schema26(database: database);
final migrator = i1.Migrator(database, schema);
await from25To26(migrator, schema);
return 26;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -13122,6 +13724,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
required Future<void> Function(i1.Migrator m, Schema24 schema) from23To24,
required Future<void> Function(i1.Migrator m, Schema25 schema) from24To25,
required Future<void> Function(i1.Migrator m, Schema26 schema) from25To26,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -13148,5 +13751,6 @@ i1.OnUpgrade stepByStep({
from22To23: from22To23,
from23To24: from23To24,
from24To25: from24To25,
from25To26: from25To26,
),
);
@@ -120,6 +120,13 @@ extension<T extends Object> on MetadataDomain<T> {
cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo),
defaultsInitialized: repo._read(.cleanupDefaultsInitialized),
),
map: .new(
relativeDays: repo._read(.mapRelativeDate),
favoritesOnly: repo._read(.mapShowFavoriteOnly),
includeArchived: repo._read(.mapIncludeArchived),
themeMode: repo._read(.mapThemeMode),
withPartners: repo._read(.mapWithPartners),
),
);
case .systemConfig:
repo._systemConfig = .new(logLevel: repo._read(.logLevel));
@@ -14,6 +14,7 @@ import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.da
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
@@ -45,25 +46,35 @@ class SyncStreamRepository extends DriftDatabaseRepository {
// foreign_keys PRAGMA is no-op within transactions
// https://www.sqlite.org/pragma.html#pragma_foreign_keys
await _db.customStatement('PRAGMA foreign_keys = OFF');
await transaction(() async {
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.authUserEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
});
await _db.customStatement('PRAGMA foreign_keys = ON');
try {
await transaction(() async {
// FK cascade (ON DELETE SET NULL) does not fire while foreign_keys = OFF,
// so null linkedRemoteAlbumId manually to avoid dangling pointers in local_album_entity.
await _db.localAlbumEntity.update().write(
const LocalAlbumEntityCompanion(linkedRemoteAlbumId: Value(null)),
);
await _db.assetFaceEntity.deleteAll();
await _db.memoryAssetEntity.deleteAll();
await _db.memoryEntity.deleteAll();
await _db.partnerEntity.deleteAll();
await _db.personEntity.deleteAll();
await _db.remoteAlbumAssetEntity.deleteAll();
await _db.remoteAlbumEntity.deleteAll();
await _db.remoteAlbumUserEntity.deleteAll();
await _db.remoteAssetEntity.deleteAll();
await _db.remoteExifEntity.deleteAll();
await _db.stackEntity.deleteAll();
await _db.authUserEntity.deleteAll();
await _db.userEntity.deleteAll();
await _db.userMetadataEntity.deleteAll();
await _db.remoteAssetCloudIdEntity.deleteAll();
await _db.assetEditEntity.deleteAll();
});
} finally {
// re-enable FK even if the transaction throws, otherwise the connection
// would be left with foreign_keys = OFF, silently disabling cascades.
await _db.customStatement('PRAGMA foreign_keys = ON');
}
});
} catch (error, stack) {
_logger.severe('Error: SyncResetV1', error, stack);
@@ -191,6 +202,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
uploadedAt: Value(asset.createdAt),
durationMs: Value(asset.duration?.toDuration()?.inMilliseconds ?? 0),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
@@ -229,6 +241,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
uploadedAt: Value(asset.createdAt),
durationMs: Value(asset.duration),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
@@ -79,6 +79,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
type: row.type,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
uploadedAt: row.uploadedAt,
thumbHash: row.thumbHash,
width: row.width,
height: row.height,
@@ -317,6 +318,17 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
origin: TimelineOrigin.remoteAssets,
);
TimelineQuery recentlyAdded(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.uploadedAt.isNotNull() &
row.deletedAt.isNull() &
row.ownerId.equals(userId) &
(row.visibility.equalsValue(AssetVisibility.timeline) | row.visibility.equalsValue(AssetVisibility.archive)),
origin: TimelineOrigin.recentlyAdded,
groupBy: groupBy,
sortBy: SortAssetsBy.uploaded,
);
TimelineQuery favorite(String userId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
filter: (row) =>
row.deletedAt.isNull() &
@@ -597,9 +609,10 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
required TimelineOrigin origin,
GroupAssetsBy groupBy = GroupAssetsBy.day,
bool joinLocal = false,
SortAssetsBy sortBy = SortAssetsBy.taken,
}) {
return (
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy),
bucketSource: () => _watchRemoteBucket(filter: filter, groupBy: groupBy, sortBy: sortBy),
assetSource: (offset, count) =>
_getRemoteAssets(filter: filter, offset: offset, count: count, joinLocal: joinLocal),
origin: origin,
@@ -609,6 +622,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
Stream<List<Bucket>> _watchRemoteBucket({
required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day,
SortAssetsBy sortBy = SortAssetsBy.taken,
}) {
if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.count(where: filter);
@@ -616,7 +630,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}
final assetCountExp = _db.remoteAssetEntity.id.count();
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy);
final dateExp = _db.remoteAssetEntity.effectiveCreatedAt(groupBy, sortBy: sortBy);
final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp])
@@ -692,8 +706,13 @@ extension on Expression<DateTime> {
}
extension on $RemoteAssetEntityTable {
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy) =>
coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
Expression<String> effectiveCreatedAt(GroupAssetsBy groupBy, {SortAssetsBy sortBy = SortAssetsBy.taken}) {
if (sortBy == SortAssetsBy.uploaded) {
return uploadedAt.dateFmt(groupBy, toLocal: true);
}
return coalesce([localDateTime.dateFmt(groupBy), createdAt.dateFmt(groupBy, toLocal: true)]);
}
}
extension on String {
+21 -8
View File
@@ -179,19 +179,32 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
final isColdStart = currentRouteName == null || currentRouteName == SplashScreenRoute.name;
PageRouteInfo? route;
if (deepLink.uri.scheme == "immich") {
final proposedRoute = await deepLinkHandler.handleScheme(deepLink, ref, isColdStart);
return proposedRoute;
route = await deepLinkHandler.handleScheme(deepLink, ref);
} else if (deepLink.uri.host == "my.immich.app") {
route = await deepLinkHandler.handleMyImmichApp(deepLink, ref);
} else {
return DeepLink.path(deepLink.path);
}
if (deepLink.uri.host == "my.immich.app") {
final proposedRoute = await deepLinkHandler.handleMyImmichApp(deepLink, ref, isColdStart);
return proposedRoute;
if (route == null) {
return isColdStart ? DeepLink.defaultPath : DeepLink.none;
}
return DeepLink.path(deepLink.path);
// We need to replace the route if the destination is the current route
if (!isColdStart) {
unawaited(
ref.read(appRouterProvider).pushAndPopUntil(route, predicate: (r) => r.settings.name != route!.routeName),
);
return DeepLink.none;
}
return DeepLink([
// we need something to segue back to if the app was cold started
if (isColdStart) const TabShellRoute(children: [MainTimelineRoute()]),
route,
]);
}
@override
+4 -2
View File
@@ -277,7 +277,7 @@ abstract class BackgroundWorkerFlutterApi {
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
Future<void> onAndroidUpload();
Future<void> onAndroidUpload(int? maxMinutes);
Future<void> cancel();
@@ -323,8 +323,10 @@ abstract class BackgroundWorkerFlutterApi {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int? arg_maxMinutes = args[0] as int?;
try {
await api.onAndroidUpload();
await api.onAndroidUpload(arg_maxMinutes);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
@@ -0,0 +1,32 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/common/mesmerizing_sliver_app_bar.dart';
@RoutePage()
class DriftRecentlyAddedPage extends StatelessWidget {
const DriftRecentlyAddedPage({super.key});
@override
Widget build(BuildContext context) {
return ProviderScope(
overrides: [
timelineServiceProvider.overrideWith((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
throw Exception('User must be logged in to access recently taken');
}
final timelineService = ref.watch(timelineFactoryProvider).recentlyAdded(user.id);
ref.onDispose(timelineService.dispose);
return timelineService;
}),
],
child: Timeline(appBar: MesmerizingSliverAppBar(title: 'recently_added'.t())),
);
}
}
@@ -13,6 +13,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.widget.dart';
@@ -879,6 +880,12 @@ class _QuickLinkList extends StatelessWidget {
isTop: true,
onTap: () => context.pushRoute(const DriftRecentlyTakenRoute()),
),
_QuickLink(
title: context.t.recently_added,
icon: Icons.upload_outlined,
isTop: true,
onTap: () => context.pushRoute(const DriftRecentlyAddedRoute()),
),
_QuickLink(
title: 'videos'.t(context: context),
icon: Icons.play_circle_outline_rounded,
@@ -12,7 +12,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
final TimelineOrigin origin;
final bool iconOnly;
final bool menuItem;
final Color? iconColor;
const OpenInBrowserActionButton({
super.key,
@@ -20,7 +19,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
required this.origin,
this.iconOnly = false,
this.menuItem = false,
this.iconColor,
});
void _onTap() async {
@@ -52,7 +50,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
return BaseActionButton(
label: 'open_in_browser'.t(context: context),
iconData: Icons.open_in_browser,
iconColor: iconColor,
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: _onTap,
@@ -4,8 +4,8 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -48,7 +48,6 @@ class ViewerKebabMenu extends ConsumerWidget {
source: ActionSource.viewer,
isCasting: isCasting,
timelineOrigin: timelineOrigin,
originalTheme: originalTheme,
);
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
@@ -67,10 +66,13 @@ class ViewerKebabMenu extends ConsumerWidget {
menuChildren: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 150),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
child: Theme(
data: originalTheme ?? context.themeData,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
),
),
),
],
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class MapState {
@@ -81,38 +81,38 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly);
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(onlyFavorites: isFavoriteOnly);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchIncludeArchived(bool isIncludeArchived) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived);
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchWithPartners(bool isWithPartners) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners);
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void setRelativeTime(int relativeDays) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays);
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays);
state = state.copyWith(relativeDays: relativeDays);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
@override
MapState build() {
final appSettingsService = ref.read(appSettingsServiceProvider);
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
return MapState(
themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)],
onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
themeMode: mapConfig.themeMode,
onlyFavorites: mapConfig.favoritesOnly,
includeArchived: mapConfig.includeArchived,
withPartners: mapConfig.withPartners,
relativeDays: mapConfig.relativeDays,
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
);
}
@@ -1,38 +1,38 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/models/map/map_state.model.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
final mapStateNotifierProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new);
class MapStateNotifier extends Notifier<MapState> {
@override
MapState build() {
final appSettingsProvider = ref.read(appSettingsServiceProvider);
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
final lightStyleUrl = ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
final darkStyleUrl = ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
return MapState(
themeMode: ThemeMode.values[appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
showFavoriteOnly: appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
withPartners: appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
relativeTime: appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
themeMode: mapConfig.themeMode,
showFavoriteOnly: mapConfig.favoritesOnly,
includeArchived: mapConfig.includeArchived,
withPartners: mapConfig.withPartners,
relativeTime: mapConfig.relativeDays,
lightStyleFetched: AsyncData(lightStyleUrl),
darkStyleFetched: AsyncData(darkStyleUrl),
);
}
void switchTheme(ThemeMode mode) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapThemeMode, mode.index);
ref.read(metadataProvider).write(MetadataKey.mapThemeMode, mode);
state = state.copyWith(themeMode: mode);
}
void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly);
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true);
}
@@ -41,17 +41,17 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchIncludeArchived(bool isIncludeArchived) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived);
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true);
}
void switchWithPartners(bool isWithPartners) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners);
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true);
}
void setRelativeTime(int relativeTime) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeTime);
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeTime);
state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true);
}
}
+2
View File
@@ -58,6 +58,7 @@ import 'package:immich_mobile/presentation/pages/drift_person.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place.page.dart';
import 'package:immich_mobile/presentation/pages/drift_place_detail.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_taken.page.dart';
import 'package:immich_mobile/presentation/pages/drift_recently_added.page.dart';
import 'package:immich_mobile/presentation/pages/drift_remote_album.page.dart';
import 'package:immich_mobile/presentation/pages/drift_trash.page.dart';
import 'package:immich_mobile/presentation/pages/drift_user_selection.page.dart';
@@ -168,6 +169,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftAssetSelectionTimelineRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftRecentlyTakenRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftRecentlyAddedRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftLocalAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftCreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftPlaceRoute.page, guards: [_authGuard, _duplicateGuard]),
+16
View File
@@ -1047,6 +1047,22 @@ class DriftPlaceRouteArgs {
int get hashCode => key.hashCode ^ currentLocation.hashCode;
}
/// generated route for
/// [DriftRecentlyAddedPage]
class DriftRecentlyAddedRoute extends PageRouteInfo<void> {
const DriftRecentlyAddedRoute({List<PageRouteInfo>? children})
: super(DriftRecentlyAddedRoute.name, initialChildren: children);
static const String name = 'DriftRecentlyAddedRoute';
static PageInfo page = PageInfo(
name,
builder: (data) {
return const DriftRecentlyAddedPage();
},
);
}
/// generated route for
/// [DriftRecentlyTakenPage]
class DriftRecentlyTakenRoute extends PageRouteInfo<void> {
@@ -30,11 +30,6 @@ enum AppSettingsEnum<T> {
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
tapToNavigate<bool>(StoreKey.tapToNavigate, "tapToNavigate", false),
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
mapwithPartners<bool>(StoreKey.mapwithPartners, null, false),
mapRelativeDate<int>(StoreKey.mapRelativeDate, null, 0),
allowSelfSignedSSLCert<bool>(StoreKey.selfSignedCert, null, false),
ignoreIcloudAssets<bool>(StoreKey.ignoreIcloudAssets, null, false),
selectedAlbumSortReverse<bool>(StoreKey.selectedAlbumSortReverse, null, true),
@@ -394,12 +394,9 @@ class BackgroundUploadService {
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final url = Uri.parse('$serverEndpoint/assets').toString();
final headers = ApiService.getRequestHeaders();
final deviceId = Store.get(StoreKey.deviceId);
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
final fieldsMap = {
'filename': originalFileName ?? filename,
'deviceAssetId': deviceAssetId ?? '',
'deviceId': deviceId,
'fileCreatedAt': createdAt.toUtc().toIso8601String(),
'fileModifiedAt': modifiedAt.toUtc().toIso8601String(),
'isFavorite': isFavorite?.toString() ?? 'false',
+11 -40
View File
@@ -45,21 +45,12 @@ class DeepLinkService {
this._currentUser,
);
DeepLink _handleColdStart(PageRouteInfo<dynamic> route, bool isColdStart) {
return DeepLink([
// we need something to segue back to if the app was cold started
// TODO: use MainTimelineRoute this when beta is default
if (isColdStart) const TabShellRoute(),
route,
]);
}
Future<DeepLink> handleScheme(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
Future<PageRouteInfo?> handleScheme(PlatformDeepLink link, WidgetRef ref) async {
// get everything after the scheme, since Uri cannot parse path
final intent = link.uri.host;
final queryParams = link.uri.queryParameters;
PageRouteInfo<dynamic>? deepLinkRoute = switch (intent) {
return switch (intent) {
"memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''),
"asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref),
"album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''),
@@ -67,20 +58,9 @@ class DeepLinkService {
"activity" => await _buildActivityDeepLink(queryParams['albumId'] ?? ''),
_ => null,
};
// Deep link resolution failed, safely handle it based on the app state
if (deepLinkRoute == null) {
if (isColdStart) {
return DeepLink.defaultPath;
}
return DeepLink.none;
}
return _handleColdStart(deepLinkRoute, isColdStart);
}
Future<DeepLink> handleMyImmichApp(PlatformDeepLink link, WidgetRef ref, bool isColdStart) async {
Future<PageRouteInfo?> handleMyImmichApp(PlatformDeepLink link, WidgetRef ref) async {
final path = link.uri.path;
const uuidRegex = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
@@ -88,29 +68,20 @@ class DeepLinkService {
final albumRegex = RegExp('/albums/($uuidRegex)');
final peopleRegex = RegExp('/people/($uuidRegex)');
PageRouteInfo<dynamic>? deepLinkRoute;
if (assetRegex.hasMatch(path)) {
final assetId = assetRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAssetDeepLink(assetId, ref);
} else if (albumRegex.hasMatch(path)) {
return _buildAssetDeepLink(assetId, ref);
}
if (albumRegex.hasMatch(path)) {
final albumId = albumRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildAlbumDeepLink(albumId);
} else if (peopleRegex.hasMatch(path)) {
return _buildAlbumDeepLink(albumId);
}
if (peopleRegex.hasMatch(path)) {
final peopleId = peopleRegex.firstMatch(path)?.group(1) ?? '';
deepLinkRoute = await _buildPeopleDeepLink(peopleId);
} else if (path == "/memory") {
deepLinkRoute = await _buildMemoryDeepLink(null);
return _buildPeopleDeepLink(peopleId);
}
// Deep link resolution failed, safely handle it based on the app state
if (deepLinkRoute == null) {
if (isColdStart) {
return DeepLink.defaultPath;
}
return DeepLink.none;
}
return _handleColdStart(deepLinkRoute, isColdStart);
return null;
}
Future<PageRouteInfo?> _buildMemoryDeepLink(String? memoryId) async {
@@ -5,8 +5,6 @@ import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
@@ -321,15 +319,12 @@ class ForegroundUploadService {
}
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
final deviceId = Store.get(StoreKey.deviceId);
final fields = {
'deviceAssetId': asset.localId!,
'deviceId': deviceId,
'fileCreatedAt': asset.createdAt.toUtc().toIso8601String(),
'fileModifiedAt': asset.updatedAt.toUtc().toIso8601String(),
'isFavorite': asset.isFavorite.toString(),
'duration': asset.duration.toString(),
'duration': (asset.durationMs ?? 0).toString(),
};
// Upload live photo video first if available
@@ -431,8 +426,6 @@ class ForegroundUploadService {
final filename = p.basename(file.path);
final fields = {
'deviceAssetId': deviceAssetId,
'deviceId': Store.get(StoreKey.deviceId),
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
'isFavorite': 'false',
+1 -6
View File
@@ -22,10 +22,10 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/open_in_browse
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_lock_folder_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_album_cover.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/similar_photos_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/set_profile_picture_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
@@ -44,7 +44,6 @@ class ActionButtonContext {
final ActionSource source;
final bool isCasting;
final TimelineOrigin timelineOrigin;
final ThemeData? originalTheme;
final int selectedCount;
const ActionButtonContext({
@@ -59,7 +58,6 @@ class ActionButtonContext {
required this.source,
this.isCasting = false,
this.timelineOrigin = TimelineOrigin.main,
this.originalTheme,
this.selectedCount = 1,
});
}
@@ -244,7 +242,6 @@ enum ActionButtonType {
origin: context.timelineOrigin,
iconOnly: iconOnly,
menuItem: menuItem,
iconColor: context.originalTheme?.iconTheme.color,
),
ActionButtonType.similarPhotos => SimilarPhotosActionButton(
assetId: (context.asset as RemoteAsset).id,
@@ -259,14 +256,12 @@ enum ActionButtonType {
ActionButtonType.openInfo => BaseActionButton(
label: 'info'.tr(),
iconData: Icons.info_outline,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () => EventStream.shared.emit(const ViewerShowDetailsEvent()),
),
ActionButtonType.viewInTimeline => BaseActionButton(
label: 'view_in_timeline'.tr(),
iconData: Icons.image_search,
iconColor: context.originalTheme?.iconTheme.color,
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: buildContext == null
+9 -1
View File
@@ -46,11 +46,13 @@ Future<void> _migrateTo25() async {
Future<void> _migrateTo26(Drift drift) async {
final migrator = _StoreMigrator(drift);
await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values);
// Theme
await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.themePrimaryColor, ImmichColorPreset.values);
await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.themeDynamic);
await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.themeColorfulInterface);
// Cleanup
final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(StoreKey.legacyCleanupKeepAlbumIds.id);
if (cleanupKeepAlbumIds != null) {
final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList();
@@ -71,6 +73,12 @@ Future<void> _migrateTo26(Drift drift) async {
);
await migrator.migrateInt(StoreKey.legacyCleanupCutoffDaysAgo, MetadataKey.cleanupCutoffDaysAgo);
await migrator.migrateBool(StoreKey.legacyCleanupDefaultsInitialized, MetadataKey.cleanupDefaultsInitialized);
// Map
await migrator.migrateBool(StoreKey.legacyMapShowFavoriteOnly, MetadataKey.mapShowFavoriteOnly);
await migrator.migrateInt(StoreKey.legacyMapRelativeDate, MetadataKey.mapRelativeDate);
await migrator.migrateBool(StoreKey.legacyMapIncludeArchived, MetadataKey.mapIncludeArchived);
await migrator.migrateEnumIndex(StoreKey.legacyMapThemeMode, MetadataKey.mapThemeMode, ThemeMode.values);
await migrator.migrateBool(StoreKey.legacyMapwithPartners, MetadataKey.mapWithPartners);
await migrator.complete();
}
+9 -38
View File
@@ -1,55 +1,26 @@
.PHONY: build watch create_app_icon create_splash build_release_android pigeon test analyze format
.PHONY: build watch create_app_icon create_splash build_release_android pigeon test analyze format migration translation
build:
dart run build_runner build --delete-conflicting-outputs
# Remove once auto_route updated to 10.1.0
dart format lib/routing/router.gr.dart
@printf "This command has been removed. Please use:\n\n mise codegen # or mise //:mobile:codegen:dart from another directory\n\n" >&2 && exit 1
pigeon:
dart run pigeon --input pigeon/native_sync_api.dart
dart run pigeon --input pigeon/local_image_api.dart
dart run pigeon --input pigeon/remote_image_api.dart
dart run pigeon --input pigeon/background_worker_api.dart
dart run pigeon --input pigeon/background_worker_lock_api.dart
dart run pigeon --input pigeon/connectivity_api.dart
dart run pigeon --input pigeon/network_api.dart
dart format lib/platform/native_sync_api.g.dart
dart format lib/platform/local_image_api.g.dart
dart format lib/platform/remote_image_api.g.dart
dart format lib/platform/background_worker_api.g.dart
dart format lib/platform/background_worker_lock_api.g.dart
dart format lib/platform/connectivity_api.g.dart
dart format lib/platform/network_api.g.dart
@printf "This command has been removed. Please use:\n\n mise pigeon # or mise //:mobile:codegen:pigeon from another directory\n\n" >&2 && exit 1
watch:
dart run build_runner watch --delete-conflicting-outputs
create_app_icon:
flutter pub run flutter_launcher_icons:main
create_splash:
flutter pub run flutter_native_splash:create
build_release_android:
flutter build appbundle
@printf "This command has been removed. Please use:\n\n mise run build:android # or mise //:mobile:build:android from another directory\n\n" >&2 && exit 1
migration:
dart run drift_dev make-migrations
@printf "This command has been removed. Please use:\n\n mise migration # or mise //:mobile:drift:migration from another directory\n\n" >&2 && exit 1
translation:
pnpm --prefix ../i18n run format:fix
dart run easy_localization:generate -S ../i18n
dart run bin/generate_keys.dart
dart format lib/generated/codegen_loader.g.dart
dart format lib/generated/translations.g.dart
@printf "This command has been removed. Please use:\n\n mise translation # or mise //:mobile:codegen:translation from another directory\n\n" >&2 && exit 1
analyze:
dart analyze --fatal-infos
dcm analyze lib --fatal-style --fatal-warnings
@printf "This command has been removed. Please use:\n\n mise analyze # or mise //:mobile:lint from another directory\n\n" >&2 && exit 1
format:
# Ignore generated files manually until https://github.com/dart-lang/dart_style/issues/864 is resolved
dart format --set-exit-if-changed $$(find lib -name '*.dart' -not \( -name 'generated_plugin_registrant.dart' -o -name '*.g.dart' -o -name '*.drift.dart' \))
@printf "This command has been removed. Please use:\n\n mise format # or mise //:mobile:format from another directory\n\n" >&2 && exit 1
test:
flutter test
@printf "This command has been removed. Please use:\n\n mise test # or mise //:mobile:test from another directory\n\n" >&2 && exit 1
+27 -80
View File
@@ -29,12 +29,15 @@ run = "dart run build_runner watch --delete-conflicting-outputs"
[tasks."codegen:pigeon"]
alias = "pigeon"
description = "Generate pigeon platform code"
depends = [
"pigeon:native-sync",
"pigeon:thumbnail",
"pigeon:background-worker",
"pigeon:background-worker-lock",
"pigeon:connectivity",
run = [
"dart run pigeon --input pigeon/native_sync_api.dart",
"dart run pigeon --input pigeon/local_image_api.dart",
"dart run pigeon --input pigeon/remote_image_api.dart",
"dart run pigeon --input pigeon/background_worker_api.dart",
"dart run pigeon --input pigeon/background_worker_lock_api.dart",
"dart run pigeon --input pigeon/connectivity_api.dart",
"dart run pigeon --input pigeon/network_api.dart",
"dart format lib/platform/native_sync_api.g.dart lib/platform/local_image_api.g.dart lib/platform/remote_image_api.g.dart lib/platform/background_worker_api.g.dart lib/platform/background_worker_lock_api.g.dart lib/platform/connectivity_api.g.dart lib/platform/network_api.g.dart",
]
[tasks."codegen:translation"]
@@ -60,13 +63,15 @@ run = "flutter pub run flutter_native_splash:create"
description = "Run mobile tests"
run = "flutter test"
[tasks.lint]
[tasks.analyze]
alias = "lint"
description = "Analyze Dart code"
depends = ["analyze:dart", "analyze:dcm"]
[tasks."lint-fix"]
[tasks."analyze-fix"]
alias = "lint-fix"
description = "Auto-fix Dart code"
depends = ["analyze:fix:dart", "analyze:fix:dcm"]
depends = ["analyze-fix:dart", "analyze-fix:dcm"]
[tasks.format]
description = "Format Dart code"
@@ -83,75 +88,6 @@ run = "dart run drift_dev make-migrations"
# Internal tasks
[tasks."pigeon:native-sync"]
description = "Generate native sync API pigeon code"
hide = true
sources = ["pigeon/native_sync_api.dart"]
outputs = [
"lib/platform/native_sync_api.g.dart",
"ios/Runner/Sync/Messages.g.swift",
"android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt",
]
run = [
"dart run pigeon --input pigeon/native_sync_api.dart",
"dart format lib/platform/native_sync_api.g.dart",
]
[tasks."pigeon:thumbnail"]
description = "Generate thumbnail API pigeon code"
hide = true
sources = ["pigeon/thumbnail_api.dart"]
outputs = [
"lib/platform/thumbnail_api.g.dart",
"ios/Runner/Images/Thumbnails.g.swift",
"android/app/src/main/kotlin/app/alextran/immich/images/Thumbnails.g.kt",
]
run = [
"dart run pigeon --input pigeon/thumbnail_api.dart",
"dart format lib/platform/thumbnail_api.g.dart",
]
[tasks."pigeon:background-worker"]
description = "Generate background worker API pigeon code"
hide = true
sources = ["pigeon/background_worker_api.dart"]
outputs = [
"lib/platform/background_worker_api.g.dart",
"ios/Runner/Background/BackgroundWorker.g.swift",
"android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt",
]
run = [
"dart run pigeon --input pigeon/background_worker_api.dart",
"dart format lib/platform/background_worker_api.g.dart",
]
[tasks."pigeon:background-worker-lock"]
description = "Generate background worker lock API pigeon code"
hide = true
sources = ["pigeon/background_worker_lock_api.dart"]
outputs = [
"lib/platform/background_worker_lock_api.g.dart",
"android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerLock.g.kt",
]
run = [
"dart run pigeon --input pigeon/background_worker_lock_api.dart",
"dart format lib/platform/background_worker_lock_api.g.dart",
]
[tasks."pigeon:connectivity"]
description = "Generate connectivity API pigeon code"
hide = true
sources = ["pigeon/connectivity_api.dart"]
outputs = [
"lib/platform/connectivity_api.g.dart",
"ios/Runner/Connectivity/Connectivity.g.swift",
"android/app/src/main/kotlin/app/alextran/immich/connectivity/Connectivity.g.kt",
]
run = [
"dart run pigeon --input pigeon/connectivity_api.dart",
"dart format lib/platform/connectivity_api.g.dart",
]
[tasks."i18n:loader"]
description = "Generate i18n loader"
hide = true
@@ -182,12 +118,23 @@ description = "Run Dart Code Metrics"
hide = true
run = "dcm analyze lib --fatal-style --fatal-warnings"
[tasks."analyze:fix:dart"]
[tasks."analyze-fix:dart"]
description = "Auto-fix Dart analysis"
hide = true
run = "dart fix --apply"
[tasks."analyze:fix:dcm"]
[tasks."analyze-fix:dcm"]
description = "Auto-fix Dart Code Metrics"
hide = true
run = "dcm fix lib"
[tasks.checklist]
run = [
{task = "codegen:pigeon" },
{task = "codegen:dart" },
{task = "codegen:translation" },
{task = "analyze" },
{task = "format" },
{task = "test" },
]
+13 -18
View File
@@ -205,8 +205,8 @@ Class | Method | HTTP request | Description
*PeopleApi* | [**updatePeople**](doc//PeopleApi.md#updatepeople) | **PUT** /people | Update people
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
*PluginsApi* | [**getPluginTriggers**](doc//PluginsApi.md#getplugintriggers) | **GET** /plugins/triggers | List all plugin triggers
*PluginsApi* | [**getPlugins**](doc//PluginsApi.md#getplugins) | **GET** /plugins | List all plugins
*PluginsApi* | [**searchPluginMethods**](doc//PluginsApi.md#searchpluginmethods) | **GET** /plugins/methods | Retrieve plugin methods
*PluginsApi* | [**searchPlugins**](doc//PluginsApi.md#searchplugins) | **GET** /plugins | List all plugins
*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
*QueuesApi* | [**getQueueJobs**](doc//QueuesApi.md#getqueuejobs) | **GET** /queues/{name}/jobs | Retrieve queue jobs
@@ -314,7 +314,9 @@ Class | Method | HTTP request | Description
*WorkflowsApi* | [**createWorkflow**](doc//WorkflowsApi.md#createworkflow) | **POST** /workflows | Create a workflow
*WorkflowsApi* | [**deleteWorkflow**](doc//WorkflowsApi.md#deleteworkflow) | **DELETE** /workflows/{id} | Delete a workflow
*WorkflowsApi* | [**getWorkflow**](doc//WorkflowsApi.md#getworkflow) | **GET** /workflows/{id} | Retrieve a workflow
*WorkflowsApi* | [**getWorkflows**](doc//WorkflowsApi.md#getworkflows) | **GET** /workflows | List all workflows
*WorkflowsApi* | [**getWorkflowForShare**](doc//WorkflowsApi.md#getworkflowforshare) | **GET** /workflows/{id}/share | Retrieve a workflow
*WorkflowsApi* | [**getWorkflowTriggers**](doc//WorkflowsApi.md#getworkflowtriggers) | **GET** /workflows/triggers | List all workflow triggers
*WorkflowsApi* | [**searchWorkflows**](doc//WorkflowsApi.md#searchworkflows) | **GET** /workflows | List all workflows
*WorkflowsApi* | [**updateWorkflow**](doc//WorkflowsApi.md#updateworkflow) | **PUT** /workflows/{id} | Update a workflow
@@ -357,7 +359,6 @@ Class | Method | HTTP request | Description
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
- [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md)
- [AssetIdErrorReason](doc//AssetIdErrorReason.md)
- [AssetIdsDto](doc//AssetIdsDto.md)
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
@@ -376,6 +377,7 @@ Class | Method | HTTP request | Description
- [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md)
- [AssetOcrResponseDto](doc//AssetOcrResponseDto.md)
- [AssetOrder](doc//AssetOrder.md)
- [AssetOrderBy](doc//AssetOrderBy.md)
- [AssetRejectReason](doc//AssetRejectReason.md)
- [AssetResponseDto](doc//AssetResponseDto.md)
- [AssetStackResponseDto](doc//AssetStackResponseDto.md)
@@ -483,21 +485,12 @@ Class | Method | HTTP request | Description
- [PersonResponseDto](doc//PersonResponseDto.md)
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md)
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
- [PinCodeChangeDto](doc//PinCodeChangeDto.md)
- [PinCodeResetDto](doc//PinCodeResetDto.md)
- [PinCodeSetupDto](doc//PinCodeSetupDto.md)
- [PlacesResponseDto](doc//PlacesResponseDto.md)
- [PluginActionResponseDto](doc//PluginActionResponseDto.md)
- [PluginContextType](doc//PluginContextType.md)
- [PluginFilterResponseDto](doc//PluginFilterResponseDto.md)
- [PluginJsonSchema](doc//PluginJsonSchema.md)
- [PluginJsonSchemaProperty](doc//PluginJsonSchemaProperty.md)
- [PluginJsonSchemaPropertyAdditionalProperties](doc//PluginJsonSchemaPropertyAdditionalProperties.md)
- [PluginJsonSchemaType](doc//PluginJsonSchemaType.md)
- [PluginMethodResponseDto](doc//PluginMethodResponseDto.md)
- [PluginResponseDto](doc//PluginResponseDto.md)
- [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md)
- [PluginTriggerType](doc//PluginTriggerType.md)
- [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueCommand](doc//QueueCommand.md)
@@ -670,12 +663,14 @@ Class | Method | HTTP request | Description
- [VersionCheckStateResponseDto](doc//VersionCheckStateResponseDto.md)
- [VideoCodec](doc//VideoCodec.md)
- [VideoContainer](doc//VideoContainer.md)
- [WorkflowActionItemDto](doc//WorkflowActionItemDto.md)
- [WorkflowActionResponseDto](doc//WorkflowActionResponseDto.md)
- [WorkflowCreateDto](doc//WorkflowCreateDto.md)
- [WorkflowFilterItemDto](doc//WorkflowFilterItemDto.md)
- [WorkflowFilterResponseDto](doc//WorkflowFilterResponseDto.md)
- [WorkflowResponseDto](doc//WorkflowResponseDto.md)
- [WorkflowShareResponseDto](doc//WorkflowShareResponseDto.md)
- [WorkflowShareStepDto](doc//WorkflowShareStepDto.md)
- [WorkflowStepDto](doc//WorkflowStepDto.md)
- [WorkflowTrigger](doc//WorkflowTrigger.md)
- [WorkflowTriggerResponseDto](doc//WorkflowTriggerResponseDto.md)
- [WorkflowType](doc//WorkflowType.md)
- [WorkflowUpdateDto](doc//WorkflowUpdateDto.md)
+8 -15
View File
@@ -105,7 +105,6 @@ part 'model/asset_face_delete_dto.dart';
part 'model/asset_face_response_dto.dart';
part 'model/asset_face_update_dto.dart';
part 'model/asset_face_update_item.dart';
part 'model/asset_face_without_person_response_dto.dart';
part 'model/asset_id_error_reason.dart';
part 'model/asset_ids_dto.dart';
part 'model/asset_ids_response_dto.dart';
@@ -124,6 +123,7 @@ part 'model/asset_metadata_upsert_dto.dart';
part 'model/asset_metadata_upsert_item_dto.dart';
part 'model/asset_ocr_response_dto.dart';
part 'model/asset_order.dart';
part 'model/asset_order_by.dart';
part 'model/asset_reject_reason.dart';
part 'model/asset_response_dto.dart';
part 'model/asset_stack_response_dto.dart';
@@ -231,21 +231,12 @@ part 'model/person_create_dto.dart';
part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart';
part 'model/person_update_dto.dart';
part 'model/person_with_faces_response_dto.dart';
part 'model/pin_code_change_dto.dart';
part 'model/pin_code_reset_dto.dart';
part 'model/pin_code_setup_dto.dart';
part 'model/places_response_dto.dart';
part 'model/plugin_action_response_dto.dart';
part 'model/plugin_context_type.dart';
part 'model/plugin_filter_response_dto.dart';
part 'model/plugin_json_schema.dart';
part 'model/plugin_json_schema_property.dart';
part 'model/plugin_json_schema_property_additional_properties.dart';
part 'model/plugin_json_schema_type.dart';
part 'model/plugin_method_response_dto.dart';
part 'model/plugin_response_dto.dart';
part 'model/plugin_trigger_response_dto.dart';
part 'model/plugin_trigger_type.dart';
part 'model/purchase_response.dart';
part 'model/purchase_update.dart';
part 'model/queue_command.dart';
@@ -418,12 +409,14 @@ part 'model/validate_library_response_dto.dart';
part 'model/version_check_state_response_dto.dart';
part 'model/video_codec.dart';
part 'model/video_container.dart';
part 'model/workflow_action_item_dto.dart';
part 'model/workflow_action_response_dto.dart';
part 'model/workflow_create_dto.dart';
part 'model/workflow_filter_item_dto.dart';
part 'model/workflow_filter_response_dto.dart';
part 'model/workflow_response_dto.dart';
part 'model/workflow_share_response_dto.dart';
part 'model/workflow_share_step_dto.dart';
part 'model/workflow_step_dto.dart';
part 'model/workflow_trigger.dart';
part 'model/workflow_trigger_response_dto.dart';
part 'model/workflow_type.dart';
part 'model/workflow_update_dto.dart';
+144 -13
View File
@@ -73,14 +73,40 @@ class PluginsApi {
return null;
}
/// List all plugin triggers
/// Retrieve plugin methods
///
/// Retrieve a list of all available plugin triggers.
/// Retrieve a list of plugin methods
///
/// Note: This method returns the HTTP [Response].
Future<Response> getPluginTriggersWithHttpInfo() async {
///
/// Parameters:
///
/// * [String] description:
///
/// * [bool] enabled:
/// Whether the plugin method is enabled
///
/// * [String] id:
/// Plugin method ID
///
/// * [String] name:
///
/// * [String] pluginName:
/// Plugin name
///
/// * [String] pluginVersion:
/// Plugin version
///
/// * [String] title:
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger
///
/// * [WorkflowType] type:
/// Workflow types
Future<Response> searchPluginMethodsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/plugins/triggers';
final apiPath = r'/plugins/methods';
// ignore: prefer_final_locals
Object? postBody;
@@ -89,6 +115,34 @@ class PluginsApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (description != null) {
queryParams.addAll(_queryParams('', 'description', description));
}
if (enabled != null) {
queryParams.addAll(_queryParams('', 'enabled', enabled));
}
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (name != null) {
queryParams.addAll(_queryParams('', 'name', name));
}
if (pluginName != null) {
queryParams.addAll(_queryParams('', 'pluginName', pluginName));
}
if (pluginVersion != null) {
queryParams.addAll(_queryParams('', 'pluginVersion', pluginVersion));
}
if (title != null) {
queryParams.addAll(_queryParams('', 'title', title));
}
if (trigger != null) {
queryParams.addAll(_queryParams('', 'trigger', trigger));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
const contentTypes = <String>[];
@@ -103,11 +157,37 @@ class PluginsApi {
);
}
/// List all plugin triggers
/// Retrieve plugin methods
///
/// Retrieve a list of all available plugin triggers.
Future<List<PluginTriggerResponseDto>?> getPluginTriggers() async {
final response = await getPluginTriggersWithHttpInfo();
/// Retrieve a list of plugin methods
///
/// Parameters:
///
/// * [String] description:
///
/// * [bool] enabled:
/// Whether the plugin method is enabled
///
/// * [String] id:
/// Plugin method ID
///
/// * [String] name:
///
/// * [String] pluginName:
/// Plugin name
///
/// * [String] pluginVersion:
/// Plugin version
///
/// * [String] title:
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger
///
/// * [WorkflowType] type:
/// Workflow types
Future<List<PluginMethodResponseDto>?> searchPluginMethods({ String? description, bool? enabled, String? id, String? name, String? pluginName, String? pluginVersion, String? title, WorkflowTrigger? trigger, WorkflowType? type, }) async {
final response = await searchPluginMethodsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, pluginName: pluginName, pluginVersion: pluginVersion, title: title, trigger: trigger, type: type, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -116,8 +196,8 @@ class PluginsApi {
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<PluginTriggerResponseDto>') as List)
.cast<PluginTriggerResponseDto>()
return (await apiClient.deserializeAsync(responseBody, 'List<PluginMethodResponseDto>') as List)
.cast<PluginMethodResponseDto>()
.toList(growable: false);
}
@@ -129,7 +209,23 @@ class PluginsApi {
/// Retrieve a list of plugins available to the authenticated user.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getPluginsWithHttpInfo() async {
///
/// Parameters:
///
/// * [String] description:
///
/// * [bool] enabled:
/// Whether the plugin is enabled
///
/// * [String] id:
/// Plugin ID
///
/// * [String] name:
///
/// * [String] title:
///
/// * [String] version:
Future<Response> searchPluginsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/plugins';
@@ -140,6 +236,25 @@ class PluginsApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (description != null) {
queryParams.addAll(_queryParams('', 'description', description));
}
if (enabled != null) {
queryParams.addAll(_queryParams('', 'enabled', enabled));
}
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (name != null) {
queryParams.addAll(_queryParams('', 'name', name));
}
if (title != null) {
queryParams.addAll(_queryParams('', 'title', title));
}
if (version != null) {
queryParams.addAll(_queryParams('', 'version', version));
}
const contentTypes = <String>[];
@@ -157,8 +272,24 @@ class PluginsApi {
/// List all plugins
///
/// Retrieve a list of plugins available to the authenticated user.
Future<List<PluginResponseDto>?> getPlugins() async {
final response = await getPluginsWithHttpInfo();
///
/// Parameters:
///
/// * [String] description:
///
/// * [bool] enabled:
/// Whether the plugin is enabled
///
/// * [String] id:
/// Plugin ID
///
/// * [String] name:
///
/// * [String] title:
///
/// * [String] version:
Future<List<PluginResponseDto>?> searchPlugins({ String? description, bool? enabled, String? id, String? name, String? title, String? version, }) async {
final response = await searchPluginsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, title: title, version: version, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
+24 -6
View File
@@ -44,6 +44,9 @@ class TimelineApi {
/// * [AssetOrder] order:
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
///
/// * [AssetOrderBy] orderBy:
/// Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)
///
/// * [String] personId:
/// Filter assets containing a specific person (face recognition)
///
@@ -66,7 +69,7 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket';
@@ -95,6 +98,9 @@ class TimelineApi {
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (orderBy != null) {
queryParams.addAll(_queryParams('', 'orderBy', orderBy));
}
if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId));
}
@@ -161,6 +167,9 @@ class TimelineApi {
/// * [AssetOrder] order:
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
///
/// * [AssetOrderBy] orderBy:
/// Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)
///
/// * [String] personId:
/// Filter assets containing a specific person (face recognition)
///
@@ -183,8 +192,8 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
Future<TimeBucketAssetResponseDto?> getTimeBucket(String timeBucket, { String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(timeBucket, albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -223,6 +232,9 @@ class TimelineApi {
/// * [AssetOrder] order:
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
///
/// * [AssetOrderBy] orderBy:
/// Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)
///
/// * [String] personId:
/// Filter assets containing a specific person (face recognition)
///
@@ -245,7 +257,7 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketsWithHttpInfo({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/buckets';
@@ -274,6 +286,9 @@ class TimelineApi {
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (orderBy != null) {
queryParams.addAll(_queryParams('', 'orderBy', orderBy));
}
if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId));
}
@@ -336,6 +351,9 @@ class TimelineApi {
/// * [AssetOrder] order:
/// Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)
///
/// * [AssetOrderBy] orderBy:
/// Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)
///
/// * [String] personId:
/// Filter assets containing a specific person (face recognition)
///
@@ -358,8 +376,8 @@ class TimelineApi {
///
/// * [bool] withStacked:
/// Include stacked assets in the response. When true, only primary assets from stacks are returned.
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
Future<List<TimeBucketsResponseDto>?> getTimeBuckets({ String? albumId, String? bbox, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, AssetOrderBy? orderBy, String? personId, String? slug, String? tagId, String? userId, AssetVisibility? visibility, bool? withCoordinates, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo( albumId: albumId, bbox: bbox, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, orderBy: orderBy, personId: personId, slug: slug, tagId: tagId, userId: userId, visibility: visibility, withCoordinates: withCoordinates, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
+164 -6
View File
@@ -178,14 +178,19 @@ class WorkflowsApi {
return null;
}
/// List all workflows
/// Retrieve a workflow
///
/// Retrieve a list of workflows available to the authenticated user.
/// Retrieve a workflow details without ids, default values, etc.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getWorkflowsWithHttpInfo() async {
///
/// Parameters:
///
/// * [String] id (required):
Future<Response> getWorkflowForShareWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows';
final apiPath = r'/workflows/{id}/share'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
@@ -208,11 +213,164 @@ class WorkflowsApi {
);
}
/// Retrieve a workflow
///
/// Retrieve a workflow details without ids, default values, etc.
///
/// Parameters:
///
/// * [String] id (required):
Future<WorkflowShareResponseDto?> getWorkflowForShare(String id,) async {
final response = await getWorkflowForShareWithHttpInfo(id,);
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), 'WorkflowShareResponseDto',) as WorkflowShareResponseDto;
}
return null;
}
/// List all workflow triggers
///
/// Retrieve a list of all available workflow triggers.
///
/// Note: This method returns the HTTP [Response].
Future<Response> getWorkflowTriggersWithHttpInfo() async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows/triggers';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// List all workflow triggers
///
/// Retrieve a list of all available workflow triggers.
Future<List<WorkflowTriggerResponseDto>?> getWorkflowTriggers() async {
final response = await getWorkflowTriggersWithHttpInfo();
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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<WorkflowTriggerResponseDto>') as List)
.cast<WorkflowTriggerResponseDto>()
.toList(growable: false);
}
return null;
}
/// List all workflows
///
/// Retrieve a list of workflows available to the authenticated user.
Future<List<WorkflowResponseDto>?> getWorkflows() async {
final response = await getWorkflowsWithHttpInfo();
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [String] description:
/// Workflow description
///
/// * [bool] enabled:
/// Workflow enabled
///
/// * [String] id:
/// Workflow ID
///
/// * [String] name:
/// Workflow name
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger type
Future<Response> searchWorkflowsWithHttpInfo({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/workflows';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (description != null) {
queryParams.addAll(_queryParams('', 'description', description));
}
if (enabled != null) {
queryParams.addAll(_queryParams('', 'enabled', enabled));
}
if (id != null) {
queryParams.addAll(_queryParams('', 'id', id));
}
if (name != null) {
queryParams.addAll(_queryParams('', 'name', name));
}
if (trigger != null) {
queryParams.addAll(_queryParams('', 'trigger', trigger));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// List all workflows
///
/// Retrieve a list of workflows available to the authenticated user.
///
/// Parameters:
///
/// * [String] description:
/// Workflow description
///
/// * [bool] enabled:
/// Workflow enabled
///
/// * [String] id:
/// Workflow ID
///
/// * [String] name:
/// Workflow name
///
/// * [WorkflowTrigger] trigger:
/// Workflow trigger type
Future<List<WorkflowResponseDto>?> searchWorkflows({ String? description, bool? enabled, String? id, String? name, WorkflowTrigger? trigger, }) async {
final response = await searchWorkflowsWithHttpInfo( description: description, enabled: enabled, id: id, name: name, trigger: trigger, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
+16 -30
View File
@@ -256,8 +256,6 @@ class ApiClient {
return AssetFaceUpdateDto.fromJson(value);
case 'AssetFaceUpdateItem':
return AssetFaceUpdateItem.fromJson(value);
case 'AssetFaceWithoutPersonResponseDto':
return AssetFaceWithoutPersonResponseDto.fromJson(value);
case 'AssetIdErrorReason':
return AssetIdErrorReasonTypeTransformer().decode(value);
case 'AssetIdsDto':
@@ -294,6 +292,8 @@ class ApiClient {
return AssetOcrResponseDto.fromJson(value);
case 'AssetOrder':
return AssetOrderTypeTransformer().decode(value);
case 'AssetOrderBy':
return AssetOrderByTypeTransformer().decode(value);
case 'AssetRejectReason':
return AssetRejectReasonTypeTransformer().decode(value);
case 'AssetResponseDto':
@@ -508,8 +508,6 @@ class ApiClient {
return PersonStatisticsResponseDto.fromJson(value);
case 'PersonUpdateDto':
return PersonUpdateDto.fromJson(value);
case 'PersonWithFacesResponseDto':
return PersonWithFacesResponseDto.fromJson(value);
case 'PinCodeChangeDto':
return PinCodeChangeDto.fromJson(value);
case 'PinCodeResetDto':
@@ -518,26 +516,10 @@ class ApiClient {
return PinCodeSetupDto.fromJson(value);
case 'PlacesResponseDto':
return PlacesResponseDto.fromJson(value);
case 'PluginActionResponseDto':
return PluginActionResponseDto.fromJson(value);
case 'PluginContextType':
return PluginContextTypeTypeTransformer().decode(value);
case 'PluginFilterResponseDto':
return PluginFilterResponseDto.fromJson(value);
case 'PluginJsonSchema':
return PluginJsonSchema.fromJson(value);
case 'PluginJsonSchemaProperty':
return PluginJsonSchemaProperty.fromJson(value);
case 'PluginJsonSchemaPropertyAdditionalProperties':
return PluginJsonSchemaPropertyAdditionalProperties.fromJson(value);
case 'PluginJsonSchemaType':
return PluginJsonSchemaTypeTypeTransformer().decode(value);
case 'PluginMethodResponseDto':
return PluginMethodResponseDto.fromJson(value);
case 'PluginResponseDto':
return PluginResponseDto.fromJson(value);
case 'PluginTriggerResponseDto':
return PluginTriggerResponseDto.fromJson(value);
case 'PluginTriggerType':
return PluginTriggerTypeTypeTransformer().decode(value);
case 'PurchaseResponse':
return PurchaseResponse.fromJson(value);
case 'PurchaseUpdate':
@@ -882,18 +864,22 @@ class ApiClient {
return VideoCodecTypeTransformer().decode(value);
case 'VideoContainer':
return VideoContainerTypeTransformer().decode(value);
case 'WorkflowActionItemDto':
return WorkflowActionItemDto.fromJson(value);
case 'WorkflowActionResponseDto':
return WorkflowActionResponseDto.fromJson(value);
case 'WorkflowCreateDto':
return WorkflowCreateDto.fromJson(value);
case 'WorkflowFilterItemDto':
return WorkflowFilterItemDto.fromJson(value);
case 'WorkflowFilterResponseDto':
return WorkflowFilterResponseDto.fromJson(value);
case 'WorkflowResponseDto':
return WorkflowResponseDto.fromJson(value);
case 'WorkflowShareResponseDto':
return WorkflowShareResponseDto.fromJson(value);
case 'WorkflowShareStepDto':
return WorkflowShareStepDto.fromJson(value);
case 'WorkflowStepDto':
return WorkflowStepDto.fromJson(value);
case 'WorkflowTrigger':
return WorkflowTriggerTypeTransformer().decode(value);
case 'WorkflowTriggerResponseDto':
return WorkflowTriggerResponseDto.fromJson(value);
case 'WorkflowType':
return WorkflowTypeTypeTransformer().decode(value);
case 'WorkflowUpdateDto':
return WorkflowUpdateDto.fromJson(value);
default:
+9 -9
View File
@@ -76,6 +76,9 @@ String parameterToString(dynamic value) {
if (value is AssetOrder) {
return AssetOrderTypeTransformer().encode(value).toString();
}
if (value is AssetOrderBy) {
return AssetOrderByTypeTransformer().encode(value).toString();
}
if (value is AssetRejectReason) {
return AssetRejectReasonTypeTransformer().encode(value).toString();
}
@@ -139,15 +142,6 @@ String parameterToString(dynamic value) {
if (value is Permission) {
return PermissionTypeTransformer().encode(value).toString();
}
if (value is PluginContextType) {
return PluginContextTypeTypeTransformer().encode(value).toString();
}
if (value is PluginJsonSchemaType) {
return PluginJsonSchemaTypeTypeTransformer().encode(value).toString();
}
if (value is PluginTriggerType) {
return PluginTriggerTypeTypeTransformer().encode(value).toString();
}
if (value is QueueCommand) {
return QueueCommandTypeTransformer().encode(value).toString();
}
@@ -205,6 +199,12 @@ String parameterToString(dynamic value) {
if (value is VideoContainer) {
return VideoContainerTypeTransformer().encode(value).toString();
}
if (value is WorkflowTrigger) {
return WorkflowTriggerTypeTransformer().encode(value).toString();
}
if (value is WorkflowType) {
return WorkflowTypeTypeTransformer().encode(value).toString();
}
return value.toString();
}
@@ -1,189 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 AssetFaceWithoutPersonResponseDto {
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance.
AssetFaceWithoutPersonResponseDto({
required this.boundingBoxX1,
required this.boundingBoxX2,
required this.boundingBoxY1,
required this.boundingBoxY2,
required this.id,
required this.imageHeight,
required this.imageWidth,
this.sourceType,
});
/// Bounding box X1 coordinate
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int boundingBoxX1;
/// Bounding box X2 coordinate
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int boundingBoxX2;
/// Bounding box Y1 coordinate
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int boundingBoxY1;
/// Bounding box Y2 coordinate
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int boundingBoxY2;
/// Face ID
String id;
/// Image height in pixels
///
/// Minimum value: 0
/// Maximum value: 9007199254740991
int imageHeight;
/// Image width in pixels
///
/// Minimum value: 0
/// Maximum value: 9007199254740991
int imageWidth;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
SourceType? sourceType;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
other.boundingBoxX1 == boundingBoxX1 &&
other.boundingBoxX2 == boundingBoxX2 &&
other.boundingBoxY1 == boundingBoxY1 &&
other.boundingBoxY2 == boundingBoxY2 &&
other.id == id &&
other.imageHeight == imageHeight &&
other.imageWidth == imageWidth &&
other.sourceType == sourceType;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(boundingBoxX1.hashCode) +
(boundingBoxX2.hashCode) +
(boundingBoxY1.hashCode) +
(boundingBoxY2.hashCode) +
(id.hashCode) +
(imageHeight.hashCode) +
(imageWidth.hashCode) +
(sourceType == null ? 0 : sourceType!.hashCode);
@override
String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, sourceType=$sourceType]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'boundingBoxX1'] = this.boundingBoxX1;
json[r'boundingBoxX2'] = this.boundingBoxX2;
json[r'boundingBoxY1'] = this.boundingBoxY1;
json[r'boundingBoxY2'] = this.boundingBoxY2;
json[r'id'] = this.id;
json[r'imageHeight'] = this.imageHeight;
json[r'imageWidth'] = this.imageWidth;
if (this.sourceType != null) {
json[r'sourceType'] = this.sourceType;
} else {
// json[r'sourceType'] = null;
}
return json;
}
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) {
upgradeDto(value, "AssetFaceWithoutPersonResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetFaceWithoutPersonResponseDto(
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
id: mapValueOfType<String>(json, r'id')!,
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
sourceType: SourceType.fromJson(json[r'sourceType']),
);
}
return null;
}
static List<AssetFaceWithoutPersonResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetFaceWithoutPersonResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetFaceWithoutPersonResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetFaceWithoutPersonResponseDto> mapFromJson(dynamic json) {
final map = <String, AssetFaceWithoutPersonResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetFaceWithoutPersonResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetFaceWithoutPersonResponseDto-objects as value to a dart map
static Map<String, List<AssetFaceWithoutPersonResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetFaceWithoutPersonResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetFaceWithoutPersonResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'boundingBoxX1',
'boundingBoxX2',
'boundingBoxY1',
'boundingBoxY2',
'id',
'imageHeight',
'imageWidth',
};
}
+85
View File
@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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;
/// Asset sorting property
class AssetOrderBy {
/// Instantiate a new enum with the provided [value].
const AssetOrderBy._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const takenAt = AssetOrderBy._(r'takenAt');
static const createdAt = AssetOrderBy._(r'createdAt');
/// List of all possible values in this [enum][AssetOrderBy].
static const values = <AssetOrderBy>[
takenAt,
createdAt,
];
static AssetOrderBy? fromJson(dynamic value) => AssetOrderByTypeTransformer().decode(value);
static List<AssetOrderBy> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetOrderBy>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetOrderBy.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetOrderBy] to String,
/// and [decode] dynamic data back to [AssetOrderBy].
class AssetOrderByTypeTransformer {
factory AssetOrderByTypeTransformer() => _instance ??= const AssetOrderByTypeTransformer._();
const AssetOrderByTypeTransformer._();
String encode(AssetOrderBy data) => data.value;
/// Decodes a [dynamic value][data] to a AssetOrderBy.
///
/// 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.
AssetOrderBy? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'takenAt': return AssetOrderBy.takenAt;
case r'createdAt': return AssetOrderBy.createdAt;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetOrderByTypeTransformer] instance.
static AssetOrderByTypeTransformer? _instance;
}
+3 -10
View File
@@ -42,7 +42,6 @@ class AssetResponseDto {
this.tags = const [],
required this.thumbhash,
required this.type,
this.unassignedFaces = const [],
required this.updatedAt,
required this.visibility,
required this.width,
@@ -139,7 +138,7 @@ class AssetResponseDto {
/// Owner user ID
String ownerId;
List<PersonWithFacesResponseDto> people;
List<PersonResponseDto> people;
/// Is resized
///
@@ -159,8 +158,6 @@ class AssetResponseDto {
AssetTypeEnum type;
List<AssetFaceWithoutPersonResponseDto> unassignedFaces;
/// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.
DateTime updatedAt;
@@ -203,7 +200,6 @@ class AssetResponseDto {
_deepEquality.equals(other.tags, tags) &&
other.thumbhash == thumbhash &&
other.type == type &&
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
other.updatedAt == updatedAt &&
other.visibility == visibility &&
other.width == width;
@@ -240,13 +236,12 @@ class AssetResponseDto {
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
(unassignedFaces.hashCode) +
(updatedAt.hashCode) +
(visibility.hashCode) +
(width == null ? 0 : width!.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isEdited=$isEdited, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -323,7 +318,6 @@ class AssetResponseDto {
// json[r'thumbhash'] = null;
}
json[r'type'] = this.type;
json[r'unassignedFaces'] = this.unassignedFaces;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
json[r'visibility'] = this.visibility;
if (this.width != null) {
@@ -366,13 +360,12 @@ class AssetResponseDto {
originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized'),
stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: mapValueOfType<int>(json, r'width'),
+3 -3
View File
@@ -77,7 +77,7 @@ class JobName {
static const versionCheck = JobName._(r'VersionCheck');
static const ocrQueueAll = JobName._(r'OcrQueueAll');
static const ocr = JobName._(r'Ocr');
static const workflowRun = JobName._(r'WorkflowRun');
static const workflowAssetCreate = JobName._(r'WorkflowAssetCreate');
/// List of all possible values in this [enum][JobName].
static const values = <JobName>[
@@ -135,7 +135,7 @@ class JobName {
versionCheck,
ocrQueueAll,
ocr,
workflowRun,
workflowAssetCreate,
];
static JobName? fromJson(dynamic value) => JobNameTypeTransformer().decode(value);
@@ -228,7 +228,7 @@ class JobNameTypeTransformer {
case r'VersionCheck': return JobName.versionCheck;
case r'OcrQueueAll': return JobName.ocrQueueAll;
case r'Ocr': return JobName.ocr;
case r'WorkflowRun': return JobName.workflowRun;
case r'WorkflowAssetCreate': return JobName.workflowAssetCreate;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
@@ -1,202 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PersonWithFacesResponseDto {
/// Returns a new [PersonWithFacesResponseDto] instance.
PersonWithFacesResponseDto({
required this.birthDate,
this.color,
this.faces = const [],
required this.id,
this.isFavorite,
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.updatedAt,
});
/// Person date of birth
DateTime? birthDate;
/// Person color (hex)
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? color;
List<AssetFaceWithoutPersonResponseDto> faces;
/// Person ID
String id;
/// Is favorite
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? isFavorite;
/// Is hidden
bool isHidden;
/// Person name
String name;
/// Thumbnail path
String thumbnailPath;
/// Last update date
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? updatedAt;
@override
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
other.birthDate == birthDate &&
other.color == color &&
_deepEquality.equals(other.faces, faces) &&
other.id == id &&
other.isFavorite == isFavorite &&
other.isHidden == isHidden &&
other.name == name &&
other.thumbnailPath == thumbnailPath &&
other.updatedAt == updatedAt;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(faces.hashCode) +
(id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden.hashCode) +
(name.hashCode) +
(thumbnailPath.hashCode) +
(updatedAt == null ? 0 : updatedAt!.hashCode);
@override
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, color=$color, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.birthDate != null) {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
json[r'faces'] = this.faces;
json[r'id'] = this.id;
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
// json[r'isFavorite'] = null;
}
json[r'isHidden'] = this.isHidden;
json[r'name'] = this.name;
json[r'thumbnailPath'] = this.thumbnailPath;
if (this.updatedAt != null) {
json[r'updatedAt'] = this.updatedAt!.toUtc().toIso8601String();
} else {
// json[r'updatedAt'] = null;
}
return json;
}
/// Returns a new [PersonWithFacesResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PersonWithFacesResponseDto? fromJson(dynamic value) {
upgradeDto(value, "PersonWithFacesResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PersonWithFacesResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
name: mapValueOfType<String>(json, r'name')!,
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
updatedAt: mapDateTime(json, r'updatedAt', r''),
);
}
return null;
}
static List<PersonWithFacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PersonWithFacesResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PersonWithFacesResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PersonWithFacesResponseDto> mapFromJson(dynamic json) {
final map = <String, PersonWithFacesResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PersonWithFacesResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PersonWithFacesResponseDto-objects as value to a dart map
static Map<String, List<PersonWithFacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PersonWithFacesResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PersonWithFacesResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'birthDate',
'faces',
'id',
'isHidden',
'name',
'thumbnailPath',
};
}
-158
View File
@@ -1,158 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginActionResponseDto {
/// Returns a new [PluginActionResponseDto] instance.
PluginActionResponseDto({
required this.description,
required this.id,
required this.methodName,
required this.pluginId,
required this.schema,
this.supportedContexts = const [],
required this.title,
});
/// Action description
String description;
/// Action ID
String id;
/// Method name
String methodName;
/// Plugin ID
String pluginId;
/// Action schema
PluginJsonSchema? schema;
/// Supported contexts
List<PluginContextType> supportedContexts;
/// Action title
String title;
@override
bool operator ==(Object other) => identical(this, other) || other is PluginActionResponseDto &&
other.description == description &&
other.id == id &&
other.methodName == methodName &&
other.pluginId == pluginId &&
other.schema == schema &&
_deepEquality.equals(other.supportedContexts, supportedContexts) &&
other.title == title;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description.hashCode) +
(id.hashCode) +
(methodName.hashCode) +
(pluginId.hashCode) +
(schema == null ? 0 : schema!.hashCode) +
(supportedContexts.hashCode) +
(title.hashCode);
@override
String toString() => 'PluginActionResponseDto[description=$description, id=$id, methodName=$methodName, pluginId=$pluginId, schema=$schema, supportedContexts=$supportedContexts, title=$title]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'description'] = this.description;
json[r'id'] = this.id;
json[r'methodName'] = this.methodName;
json[r'pluginId'] = this.pluginId;
if (this.schema != null) {
json[r'schema'] = this.schema;
} else {
// json[r'schema'] = null;
}
json[r'supportedContexts'] = this.supportedContexts;
json[r'title'] = this.title;
return json;
}
/// Returns a new [PluginActionResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PluginActionResponseDto? fromJson(dynamic value) {
upgradeDto(value, "PluginActionResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PluginActionResponseDto(
description: mapValueOfType<String>(json, r'description')!,
id: mapValueOfType<String>(json, r'id')!,
methodName: mapValueOfType<String>(json, r'methodName')!,
pluginId: mapValueOfType<String>(json, r'pluginId')!,
schema: PluginJsonSchema.fromJson(json[r'schema']),
supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
title: mapValueOfType<String>(json, r'title')!,
);
}
return null;
}
static List<PluginActionResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginActionResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginActionResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PluginActionResponseDto> mapFromJson(dynamic json) {
final map = <String, PluginActionResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PluginActionResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PluginActionResponseDto-objects as value to a dart map
static Map<String, List<PluginActionResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PluginActionResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PluginActionResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'description',
'id',
'methodName',
'pluginId',
'schema',
'supportedContexts',
'title',
};
}
-88
View File
@@ -1,88 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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;
/// Plugin context
class PluginContextType {
/// Instantiate a new enum with the provided [value].
const PluginContextType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const asset = PluginContextType._(r'asset');
static const album = PluginContextType._(r'album');
static const person = PluginContextType._(r'person');
/// List of all possible values in this [enum][PluginContextType].
static const values = <PluginContextType>[
asset,
album,
person,
];
static PluginContextType? fromJson(dynamic value) => PluginContextTypeTypeTransformer().decode(value);
static List<PluginContextType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginContextType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginContextType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PluginContextType] to String,
/// and [decode] dynamic data back to [PluginContextType].
class PluginContextTypeTypeTransformer {
factory PluginContextTypeTypeTransformer() => _instance ??= const PluginContextTypeTypeTransformer._();
const PluginContextTypeTypeTransformer._();
String encode(PluginContextType data) => data.value;
/// Decodes a [dynamic value][data] to a PluginContextType.
///
/// 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.
PluginContextType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'asset': return PluginContextType.asset;
case r'album': return PluginContextType.album;
case r'person': return PluginContextType.person;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PluginContextTypeTypeTransformer] instance.
static PluginContextTypeTypeTransformer? _instance;
}
-158
View File
@@ -1,158 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginFilterResponseDto {
/// Returns a new [PluginFilterResponseDto] instance.
PluginFilterResponseDto({
required this.description,
required this.id,
required this.methodName,
required this.pluginId,
required this.schema,
this.supportedContexts = const [],
required this.title,
});
/// Filter description
String description;
/// Filter ID
String id;
/// Method name
String methodName;
/// Plugin ID
String pluginId;
/// Filter schema
PluginJsonSchema? schema;
/// Supported contexts
List<PluginContextType> supportedContexts;
/// Filter title
String title;
@override
bool operator ==(Object other) => identical(this, other) || other is PluginFilterResponseDto &&
other.description == description &&
other.id == id &&
other.methodName == methodName &&
other.pluginId == pluginId &&
other.schema == schema &&
_deepEquality.equals(other.supportedContexts, supportedContexts) &&
other.title == title;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description.hashCode) +
(id.hashCode) +
(methodName.hashCode) +
(pluginId.hashCode) +
(schema == null ? 0 : schema!.hashCode) +
(supportedContexts.hashCode) +
(title.hashCode);
@override
String toString() => 'PluginFilterResponseDto[description=$description, id=$id, methodName=$methodName, pluginId=$pluginId, schema=$schema, supportedContexts=$supportedContexts, title=$title]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'description'] = this.description;
json[r'id'] = this.id;
json[r'methodName'] = this.methodName;
json[r'pluginId'] = this.pluginId;
if (this.schema != null) {
json[r'schema'] = this.schema;
} else {
// json[r'schema'] = null;
}
json[r'supportedContexts'] = this.supportedContexts;
json[r'title'] = this.title;
return json;
}
/// Returns a new [PluginFilterResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PluginFilterResponseDto? fromJson(dynamic value) {
upgradeDto(value, "PluginFilterResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PluginFilterResponseDto(
description: mapValueOfType<String>(json, r'description')!,
id: mapValueOfType<String>(json, r'id')!,
methodName: mapValueOfType<String>(json, r'methodName')!,
pluginId: mapValueOfType<String>(json, r'pluginId')!,
schema: PluginJsonSchema.fromJson(json[r'schema']),
supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']),
title: mapValueOfType<String>(json, r'title')!,
);
}
return null;
}
static List<PluginFilterResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginFilterResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginFilterResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PluginFilterResponseDto> mapFromJson(dynamic json) {
final map = <String, PluginFilterResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PluginFilterResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PluginFilterResponseDto-objects as value to a dart map
static Map<String, List<PluginFilterResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PluginFilterResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PluginFilterResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'description',
'id',
'methodName',
'pluginId',
'schema',
'supportedContexts',
'title',
};
}
-158
View File
@@ -1,158 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginJsonSchema {
/// Returns a new [PluginJsonSchema] instance.
PluginJsonSchema({
this.additionalProperties,
this.description,
this.properties = const {},
this.required_ = const [],
this.type,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? additionalProperties;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? description;
Map<String, PluginJsonSchemaProperty> properties;
List<String> required_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaType? type;
@override
bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchema &&
other.additionalProperties == additionalProperties &&
other.description == description &&
_deepEquality.equals(other.properties, properties) &&
_deepEquality.equals(other.required_, required_) &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(additionalProperties == null ? 0 : additionalProperties!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(properties.hashCode) +
(required_.hashCode) +
(type == null ? 0 : type!.hashCode);
@override
String toString() => 'PluginJsonSchema[additionalProperties=$additionalProperties, description=$description, properties=$properties, required_=$required_, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.additionalProperties != null) {
json[r'additionalProperties'] = this.additionalProperties;
} else {
// json[r'additionalProperties'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'properties'] = this.properties;
json[r'required'] = this.required_;
if (this.type != null) {
json[r'type'] = this.type;
} else {
// json[r'type'] = null;
}
return json;
}
/// Returns a new [PluginJsonSchema] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PluginJsonSchema? fromJson(dynamic value) {
upgradeDto(value, "PluginJsonSchema");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PluginJsonSchema(
additionalProperties: mapValueOfType<bool>(json, r'additionalProperties'),
description: mapValueOfType<String>(json, r'description'),
properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']),
required_: json[r'required'] is Iterable
? (json[r'required'] as Iterable).cast<String>().toList(growable: false)
: const [],
type: PluginJsonSchemaType.fromJson(json[r'type']),
);
}
return null;
}
static List<PluginJsonSchema> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginJsonSchema>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginJsonSchema.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PluginJsonSchema> mapFromJson(dynamic json) {
final map = <String, PluginJsonSchema>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PluginJsonSchema.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PluginJsonSchema-objects as value to a dart map
static Map<String, List<PluginJsonSchema>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PluginJsonSchema>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PluginJsonSchema.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
-195
View File
@@ -1,195 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginJsonSchemaProperty {
/// Returns a new [PluginJsonSchemaProperty] instance.
PluginJsonSchemaProperty({
this.additionalProperties,
this.default_,
this.description,
this.enum_ = const [],
this.items,
this.properties = const {},
this.required_ = const [],
this.type,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaPropertyAdditionalProperties? additionalProperties;
Object? default_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? description;
List<String> enum_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaProperty? items;
Map<String, PluginJsonSchemaProperty> properties;
List<String> required_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaType? type;
@override
bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaProperty &&
other.additionalProperties == additionalProperties &&
other.default_ == default_ &&
other.description == description &&
_deepEquality.equals(other.enum_, enum_) &&
other.items == items &&
_deepEquality.equals(other.properties, properties) &&
_deepEquality.equals(other.required_, required_) &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(additionalProperties == null ? 0 : additionalProperties!.hashCode) +
(default_ == null ? 0 : default_!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(enum_.hashCode) +
(items == null ? 0 : items!.hashCode) +
(properties.hashCode) +
(required_.hashCode) +
(type == null ? 0 : type!.hashCode);
@override
String toString() => 'PluginJsonSchemaProperty[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.additionalProperties != null) {
json[r'additionalProperties'] = this.additionalProperties;
} else {
// json[r'additionalProperties'] = null;
}
if (this.default_ != null) {
json[r'default'] = this.default_;
} else {
// json[r'default'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'enum'] = this.enum_;
if (this.items != null) {
json[r'items'] = this.items;
} else {
// json[r'items'] = null;
}
json[r'properties'] = this.properties;
json[r'required'] = this.required_;
if (this.type != null) {
json[r'type'] = this.type;
} else {
// json[r'type'] = null;
}
return json;
}
/// Returns a new [PluginJsonSchemaProperty] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PluginJsonSchemaProperty? fromJson(dynamic value) {
upgradeDto(value, "PluginJsonSchemaProperty");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PluginJsonSchemaProperty(
additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']),
default_: mapValueOfType<Object>(json, r'default'),
description: mapValueOfType<String>(json, r'description'),
enum_: json[r'enum'] is Iterable
? (json[r'enum'] as Iterable).cast<String>().toList(growable: false)
: const [],
items: PluginJsonSchemaProperty.fromJson(json[r'items']),
properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']),
required_: json[r'required'] is Iterable
? (json[r'required'] as Iterable).cast<String>().toList(growable: false)
: const [],
type: PluginJsonSchemaType.fromJson(json[r'type']),
);
}
return null;
}
static List<PluginJsonSchemaProperty> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginJsonSchemaProperty>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginJsonSchemaProperty.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PluginJsonSchemaProperty> mapFromJson(dynamic json) {
final map = <String, PluginJsonSchemaProperty>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PluginJsonSchemaProperty.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PluginJsonSchemaProperty-objects as value to a dart map
static Map<String, List<PluginJsonSchemaProperty>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PluginJsonSchemaProperty>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PluginJsonSchemaProperty.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
@@ -1,195 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginJsonSchemaPropertyAdditionalProperties {
/// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance.
PluginJsonSchemaPropertyAdditionalProperties({
this.additionalProperties,
this.default_,
this.description,
this.enum_ = const [],
this.items,
this.properties = const {},
this.required_ = const [],
this.type,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaPropertyAdditionalProperties? additionalProperties;
Object? default_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? description;
List<String> enum_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaProperty? items;
Map<String, PluginJsonSchemaProperty> properties;
List<String> required_;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
PluginJsonSchemaType? type;
@override
bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaPropertyAdditionalProperties &&
other.additionalProperties == additionalProperties &&
other.default_ == default_ &&
other.description == description &&
_deepEquality.equals(other.enum_, enum_) &&
other.items == items &&
_deepEquality.equals(other.properties, properties) &&
_deepEquality.equals(other.required_, required_) &&
other.type == type;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(additionalProperties == null ? 0 : additionalProperties!.hashCode) +
(default_ == null ? 0 : default_!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(enum_.hashCode) +
(items == null ? 0 : items!.hashCode) +
(properties.hashCode) +
(required_.hashCode) +
(type == null ? 0 : type!.hashCode);
@override
String toString() => 'PluginJsonSchemaPropertyAdditionalProperties[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.additionalProperties != null) {
json[r'additionalProperties'] = this.additionalProperties;
} else {
// json[r'additionalProperties'] = null;
}
if (this.default_ != null) {
json[r'default'] = this.default_;
} else {
// json[r'default'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
json[r'enum'] = this.enum_;
if (this.items != null) {
json[r'items'] = this.items;
} else {
// json[r'items'] = null;
}
json[r'properties'] = this.properties;
json[r'required'] = this.required_;
if (this.type != null) {
json[r'type'] = this.type;
} else {
// json[r'type'] = null;
}
return json;
}
/// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PluginJsonSchemaPropertyAdditionalProperties? fromJson(dynamic value) {
upgradeDto(value, "PluginJsonSchemaPropertyAdditionalProperties");
if (value is Map) {
final json = value.cast<String, dynamic>();
return PluginJsonSchemaPropertyAdditionalProperties(
additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']),
default_: mapValueOfType<Object>(json, r'default'),
description: mapValueOfType<String>(json, r'description'),
enum_: json[r'enum'] is Iterable
? (json[r'enum'] as Iterable).cast<String>().toList(growable: false)
: const [],
items: PluginJsonSchemaProperty.fromJson(json[r'items']),
properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']),
required_: json[r'required'] is Iterable
? (json[r'required'] as Iterable).cast<String>().toList(growable: false)
: const [],
type: PluginJsonSchemaType.fromJson(json[r'type']),
);
}
return null;
}
static List<PluginJsonSchemaPropertyAdditionalProperties> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginJsonSchemaPropertyAdditionalProperties>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PluginJsonSchemaPropertyAdditionalProperties> mapFromJson(dynamic json) {
final map = <String, PluginJsonSchemaPropertyAdditionalProperties>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PluginJsonSchemaPropertyAdditionalProperties-objects as value to a dart map
static Map<String, List<PluginJsonSchemaPropertyAdditionalProperties>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PluginJsonSchemaPropertyAdditionalProperties>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PluginJsonSchemaPropertyAdditionalProperties.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}
-100
View File
@@ -1,100 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 PluginJsonSchemaType {
/// Instantiate a new enum with the provided [value].
const PluginJsonSchemaType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const string = PluginJsonSchemaType._(r'string');
static const number = PluginJsonSchemaType._(r'number');
static const integer = PluginJsonSchemaType._(r'integer');
static const boolean = PluginJsonSchemaType._(r'boolean');
static const object = PluginJsonSchemaType._(r'object');
static const array = PluginJsonSchemaType._(r'array');
static const null_ = PluginJsonSchemaType._(r'null');
/// List of all possible values in this [enum][PluginJsonSchemaType].
static const values = <PluginJsonSchemaType>[
string,
number,
integer,
boolean,
object,
array,
null_,
];
static PluginJsonSchemaType? fromJson(dynamic value) => PluginJsonSchemaTypeTypeTransformer().decode(value);
static List<PluginJsonSchemaType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PluginJsonSchemaType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PluginJsonSchemaType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PluginJsonSchemaType] to String,
/// and [decode] dynamic data back to [PluginJsonSchemaType].
class PluginJsonSchemaTypeTypeTransformer {
factory PluginJsonSchemaTypeTypeTransformer() => _instance ??= const PluginJsonSchemaTypeTypeTransformer._();
const PluginJsonSchemaTypeTypeTransformer._();
String encode(PluginJsonSchemaType data) => data.value;
/// Decodes a [dynamic value][data] to a PluginJsonSchemaType.
///
/// 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.
PluginJsonSchemaType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'string': return PluginJsonSchemaType.string;
case r'number': return PluginJsonSchemaType.number;
case r'integer': return PluginJsonSchemaType.integer;
case r'boolean': return PluginJsonSchemaType.boolean;
case r'object': return PluginJsonSchemaType.object;
case r'array': return PluginJsonSchemaType.array;
case r'null': return PluginJsonSchemaType.null_;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PluginJsonSchemaTypeTypeTransformer] instance.
static PluginJsonSchemaTypeTypeTransformer? _instance;
}

Some files were not shown because too many files have changed in this diff Show More