Compare commits

..

39 Commits

Author SHA1 Message Date
shenlong-tanwen 318313684c rename metadata to settings 2026-05-30 01:36:19 +05:30
Jason Rasmussen da8505f61d feat: more plugin triggers and methods (#28690) 2026-05-29 14:02:07 -04:00
Alex 58586483dc feat: render album's name in workflow step card (#28680)
* feat: render album name in step card body

* clean up

* i18n
2026-05-29 10:37:37 -05:00
pneuly a838167f11 fix(ml): pass model_root_dir to OcrOptions for RapidOCR compatibility (#28610)
* fix(ml): pass model_root_dir to OcrOptions for RapidOCR compatibility

Fix a TypeError (Path(None)) when the OCR model is invoked, caused by an upstream change in RapidOCR v3.8.1 (RapidAI/RapidOCR@8ea9626).
RapidOCR now internally calls `Path(cfg.get("model_root_dir"))`. Since `model_root_dir` was missing from `OcrOptions`, it evaluated to `None` and triggered a `TypeError: argument should be a str or an os.PathLike`.
This fix adds the missing `model_root_dir` argument to prevent the error.
Ref: #28331

* fix(ml-test): update OCR tests for RapidOCR schema change

* chore(ml-test): remove unused `cache_dir` parameter from `TextRecognizer`

* Revert "chore(ml-test): remove unused `cache_dir` parameter from `TextRecognizer`"

This reverts commit 007ad7b3f2.

* fix(ml): use self.cache_dir for model_root_dir in OcrOptions
2026-05-28 22:54:04 -04:00
Mert b189fc571c fix: make ts a peer dependency for swagger (#28677)
make ts a peer dependency
2026-05-28 22:04:25 +00:00
Jason Rasmussen 96923f6115 refactor: plugin sdk types (#28674) 2026-05-28 22:04:15 +00:00
shenlong 0d6cce4a5b fix: api repositories using stale endpoint (#28667)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-28 16:44:11 -05:00
shenlong 55947cb227 refactor: drop metadata scope (#28668)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-28 16:42:59 -05:00
Jason Rasmussen 8783180cf3 refactor: plugin manifest (#28673) 2026-05-28 17:23:49 -04:00
Jason Rasmussen 134c0d4dfb feat: search by album name and id (#28672) 2026-05-28 17:01:47 -04:00
Alex aecf8ec88b fix: timeline scroll flicker (#28653)
* test: fix scroll flicker

* lint
2026-05-28 08:20:54 -05:00
Daniel Dietzler bcff1d42b0 chore: migrate more make targets (#28663) 2026-05-28 08:33:57 -04:00
Min Idzelis 1bd367bd51 refactor(web): replace per-asset viewport proximity with day-tier active indices (#28597) 2026-05-28 11:44:18 +02:00
Daniel Dietzler 725f266b81 chore: migrate more make targets to mise (#28651) 2026-05-28 11:31:02 +02:00
Daniel Dietzler d08e3de207 fix: e2e linting (#28659) 2026-05-28 11:12:26 +02:00
Timon 26714f6bfe fix(server): prevent locked assets from leaking to partners (#28652)
* fix(server): prevent locked assets from leaking to partners

* fix tests
2026-05-27 17:33:49 -04:00
Lauritz Tieste a5ce3fc927 fix: Refresh local album overview page after asset deletion (#28586)
fix: invalidate local album provider on asset delete
2026-05-28 01:20:32 +05:30
shenlong 3b23f71a3f refactor: cleanup metadata (#28485)
jason-ify metadata

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-28 01:19:25 +05:30
shenlong dec33cadd9 fix: verify form disposal before notifyListeners (#28578)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-27 19:45:48 +00:00
Daniel Dietzler 80c15a5e27 fix: workflow drag and drop (#28650) 2026-05-27 14:05:38 -05:00
Yaros 936c28a40b feat(mobile): improve downloading algorithm for sharing (#27312)
* feat(mobile): better downloading while sharing

* chore: separate download group

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-27 17:00:49 +00:00
Spencer Stingley 1a837a28ac fix: dev container properly builds @immich/plugin-sdk for import (#28620)
Co-authored-by: Spencer Stingley <accounts@blankcanvas.io>
2026-05-27 12:41:35 -04:00
shenlong 8d5d12b108 chore: upgrade flutter to 3.44.0 (#28537)
* chore: upgrade flutter to 3.44.0

# Conflicts:
#	mise.lock

* static analysis fix

* fix ios ci

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-27 11:14:44 -05:00
Daniel Dietzler dd7a94135f refactor: workflow components (#28648) 2026-05-27 18:08:12 +02:00
Jason Rasmussen 1acc511b5c chore: install mise in Dockerfile.dev (#28649) 2026-05-27 11:43:17 -04:00
Daniel Dietzler 452e88267a fix: strip metadata from timeline responses for shared links without exif sharing (#28644) 2026-05-27 17:29:37 +02:00
Timon b941108cbd chore: update documentation to use mise commands (#28515) 2026-05-27 10:33:23 -04:00
Jason Rasmussen e46f2843f7 refactor: asset create event (#28647) 2026-05-27 10:28:02 -04:00
Jason Rasmussen cf991e7b1b feat: workflow actions (#28639) 2026-05-27 10:24:31 -04:00
Thomas van Gemert 748a13104a chore(docs): update FAQ with profile picture change instructions (#28634)
Update FAQ with profile picture change instructions

Based on this discussion (https://github.com/immich-app/immich/discussions/27168) it seems I am not the only one confused by how Immich lets you change your profile picture. An addition to the FAQ will help. I also added another horizontal separator to be consistent with the rest of the document.
2026-05-27 09:17:12 -05:00
Brandon Wees 2dd6b47714 fix: OCR bounding box positioning (#28568) 2026-05-27 12:01:30 +02:00
Alex 8682be4774 feat: workflow template (#28553)
* wip: confirm before existing and disable/enable save button condition

* fix: get correct workflow detail

* wip: add back workflow summary

* wip: add back json editor

* wip: step property badge

* wip: redesign card flow

* wip: redesign card flow

* redesign workflow summary

* wworkflow summary styling

* wip

* drag and drop

* list redesign

* refactor

* refactor

* remove deadcode

* refactor

* insert steps

* push down when dropped

* feat: workflow template

* simplify

* move template to manifest

* feat: hash manifest file

* fix: template column

* fix: migration

* fix: workflow lookup

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-26 16:47:05 -04:00
Brandon Wees dc66892ca1 fix: await sync asset v2 (#28569)
* fix: await sync asset v2

* fix: support previous server versions for edit ready events
2026-05-26 15:43:12 -04:00
Fabian Wimberger 53a24783f5 fix(ml): stabilize MIGraphX inference (#28444)
* fix: stabilize ROCm MIGraphX inference

Serialize MIGraphX session runs so lazy compiles cannot overlap within a worker.

Use a fixed face-recognition batch size for MIGraphX to avoid compiling a new program for each detected face count.

* fix(ml): increase ROCm worker timeout

* fix(ml): narrow MIGraphX compile locking

* docs: format environment variables table

* docs: apply prettier to environment variables table
2026-05-26 18:41:56 +00:00
Alex 0546bc900c chore: workflow UI (#28536)
* wip: confirm before existing and disable/enable save button condition

* fix: get correct workflow detail

* wip: add back workflow summary

* wip: add back json editor

* wip: step property badge

* wip: redesign card flow

* wip: redesign card flow

* redesign workflow summary

* wworkflow summary styling

* wip

* drag and drop

* list redesign

* refactor

* refactor

* remove deadcode

* refactor

* insert steps

* push down when dropped

* fix: query by workflow id

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-26 14:06:20 -04:00
Brandon Wees 7c25bcc0a7 refactor: use ControlBar UI Library component (#28567)
* refactor: use ControlBar UI Library component

* chore: ci fix

* fix: memory viewer bar

* chore: rework e2e test

* chore: more ci fixes
2026-05-26 12:03:37 -04:00
Luis Nachtigall 7905853639 fix(mobile): preserve zoom level during image loading and live photo playback (#27960)
* fix(mobile): preserve zoom level when new images load in asset viewer

* fix(mobile): use actual child size for live photo

* revert fixes

* fix(mobile): keep zoom consistent when scale boundaries change

* fix(mobile): simplify scale handling in photo_view_core.dart
2026-05-26 21:10:02 +05:30
Mees Frensel 073dcc1fbe chore(server): deprecate total field of asset search response (#28551) 2026-05-26 16:20:24 +02:00
renovate[bot] ccdaa4223c chore(deps): update github-actions (#28623) 2026-05-26 15:04:51 +02:00
245 changed files with 17687 additions and 2544 deletions
@@ -8,6 +8,8 @@ log "Preparing Immich Web Frontend"
log ""
run_cmd pnpm --filter @immich/sdk install
run_cmd pnpm --filter @immich/sdk build
run_cmd pnpm --filter @immich/plugin-sdk install
run_cmd pnpm --filter @immich/plugin-sdk build
run_cmd pnpm --filter immich-web install
log "Starting Immich Web Frontend"
+5 -1
View File
@@ -230,8 +230,12 @@ jobs:
- name: Generate platform APIs
run: mise //mobile:codegen:pigeon
- name: Resolve iOS Swift Packages
working-directory: ./mobile
run: flutter build ios --config-only --no-codesign
- name: Setup Ruby
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
with:
ruby-version: '3.3'
bundler-cache: true
+3 -3
View File
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
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@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
# ️ 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@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: '/language:${{matrix.language}}'
+15 -24
View File
@@ -1,46 +1,46 @@
dev:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
dev-down:
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
dev-update:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
dev-scale:
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
dev-docs:
npm --prefix docs run start
.PHONY: e2e
e2e:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
e2e-dev:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.dev.yml up --remove-orphans
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
e2e-update:
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
e2e-down:
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
prod:
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
prod-down:
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
prod-scale:
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
.PHONY: open-api
open-api:
@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
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
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
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
renovate:
@@ -52,16 +52,7 @@ renovate:
MODULES = e2e server web cli sdk docs .github
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
pnpm --filter immich-e2e run test
pnpm --filter immich-e2e run test:web
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
clean:
find . -name "node_modules" -type d -prune -exec rm -rf {} +
find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
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
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
+5 -10
View File
@@ -21,7 +21,7 @@ services:
volumes:
- ..:/usr/src/app
# - ../../ui:/usr/src/ui
- pnpm_cache:/buildcache/pnpm_cache
- build_cache:/buildcache
- 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
@@ -45,11 +45,11 @@ services:
target: dev
command:
- |
pnpm install
mise install
touch /tmp/init-complete
exec tail -f /dev/null
volumes:
- pnpm_store_server:/buildcache/pnpm-store
- build_cache:/buildcache
restart: 'no'
healthcheck:
test: ['CMD', 'test', '-f', '/tmp/init-complete']
@@ -73,7 +73,6 @@ services:
volumes:
- ${UPLOAD_LOCATION}/photos:/data
- /etc/localtime:/etc/localtime:ro
- pnpm_store_server:/buildcache/pnpm-store
- ../packages/plugin-core:/build/plugins/immich-plugin-core
env_file:
- .env
@@ -122,8 +121,6 @@ services:
ports:
- 3000:3000
- 24678:24678
volumes:
- pnpm_store_web:/buildcache/pnpm-store
restart: unless-stopped
depends_on:
immich-init:
@@ -157,7 +154,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
@@ -203,9 +200,7 @@ volumes:
model_cache:
prometheus_data:
grafana_data:
pnpm_cache:
pnpm_store_server:
pnpm_store_web:
build_cache:
server_node_modules:
web_node_modules:
github_node_modules:
+1 -1
View File
@@ -56,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
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:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
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:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
restart: always
+6
View File
@@ -26,6 +26,8 @@ For organizations seeking to resell Immich, we have established the following gu
When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app
---
## User
### How can I reset the admin password?
@@ -36,6 +38,10 @@ The admin password can be reset by running the [reset-admin-password](/administr
You can see the list of all users by running [list-users](/administration/server-commands.md) Command on the Immich-server.
### How can I change my profile picture?
View a single photo, press the three dots in the top-right to show context menu, and select "Set as profile picture". In the pop-up, use your mouse scroll wheel to zoom in the picture until it completely fills the circle. Click and drag the picture to align it to your liking. Press "Save" to save your changes.
---
## Mobile App
+2 -2
View File
@@ -5,7 +5,7 @@ After making any changes in the `server/src/schema`, a database migration need t
1. Run the command
```bash
pnpm run migrations:generate <migration-name>
mise //server:migrations generate <migration-name>
```
2. Check if the migration file makes sense.
@@ -18,7 +18,7 @@ The server will automatically detect `*.ts` file changes and restart. Part of th
If you need to undo the most recently applied migration—for example, when developing or testing on schema changes—run:
```bash
pnpm run migrations:revert
mise //server:migrations revert
```
This command rolls back the latest migration and brings the database schema back to its previous state.
+19 -30
View File
@@ -252,44 +252,33 @@ To connect the mobile app to your Dev Container:
The Dev Container supports multiple ways to run tests:
#### Using Mise Commands (Recommended)
```bash
# Run tests for specific components
mise run checklist # in `server/`, `web/`, `packages/cli`
# Server
mise //server:test # unit tests
mise //server:test-medium # medium / integration tests
# Web
mise //web:test # unit tests
# E2E
mise //e2e:test # API tests
mise //e2e:test-web # web UI tests (Playwright)
# Run all checks for a component
mise //server:checklist
mise //web:checklist
```
#### Using PNPM Directly
```bash
# Server tests
cd /workspaces/immich/server
pnpm test # Run all tests
pnpm run test:medium # Medium tests (integration tests)
pnpm run test:watch # Watch mode
pnpm run test:cov # Coverage report
# Web tests
cd /workspaces/immich/web
pnpm test # Run all tests
pnpm run test:watch # Watch mode
# E2E tests
cd /workspaces/immich/e2e
pnpm run test # Run API tests
pnpm run test:web # Run web UI tests
```
### Additional Make Commands
### Additional Commands
```bash
# API generation
make open-api # Generate OpenAPI specs
make open-api-typescript # Generate TypeScript SDK
make open-api-dart # Generate Dart SDK
mise //:open-api # Generate OpenAPI specs
mise //:open-api-typescript # Generate TypeScript SDK
mise //:open-api-dart # Generate Dart SDK
# Database
mise sql # Sync database schema
mise //server:sql # Sync database schema
```
### Debugging
+32 -13
View File
@@ -8,34 +8,42 @@ When contributing code through a pull request, please check the following:
## Web Checks
- [ ] `pnpm run lint` (linting via ESLint)
- [ ] `pnpm run format` (formatting via Prettier)
- [ ] `pnpm run check:svelte` (Type checking via SvelteKit)
- [ ] `pnpm run check:typescript` (check typescript)
- [ ] `pnpm test` (unit tests)
- [ ] `mise //web:lint` (linting via ESLint)
- [ ] `mise //web:format` (formatting via Prettier)
- [ ] `mise //web:check-svelte` (type checking via SvelteKit)
- [ ] `mise //web:check-typescript` (type checking via `tsc`)
- [ ] `mise //web:test` (unit tests)
:::tip AIO
Run all web checks with `pnpm run check:all`
Run all web checks with `mise //web:checklist`
:::
:::tip Auto Fix
Use `mise //web:lint-fix` and `mise //web:format-fix` to automatically correct some issues.
:::
## Documentation
- [ ] `pnpm run format` (formatting via Prettier)
- [ ] `mise //docs:format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
:::tip Auto Fix
Use `mise //docs:format-fix` to automatically fix formatting.
:::
## Server Checks
- [ ] `pnpm run lint` (linting via ESLint)
- [ ] `pnpm run format` (formatting via Prettier)
- [ ] `pnpm run check` (Type checking via `tsc`)
- [ ] `pnpm test` (unit tests)
- [ ] `mise //server:lint` (linting via ESLint)
- [ ] `mise //server:format` (formatting via Prettier)
- [ ] `mise //server:check` (type checking via `tsc`)
- [ ] `mise //server:test` (unit tests)
:::tip AIO
Run all server checks with `pnpm run check:all`
Run all server checks with `mise //server:checklist`
:::
:::tip Auto Fix
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
Use `mise //server:lint-fix` and `mise //server:format-fix` to automatically correct some issues.
:::
## Mobile Checklist
@@ -53,6 +61,17 @@ Run all these commands at once with `mise //mobile:checklist`
You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
:::
## Machine Learning Checklist
- [ ] `mise //machine-learning:lint` (linting via ruff)
- [ ] `mise //machine-learning:format` (formatting via ruff)
- [ ] `mise //machine-learning:check` (type checking via mypy)
- [ ] `mise //machine-learning:test` (unit tests via pytest)
:::tip AIO
Run all machine learning checks with `mise //machine-learning:checklist`
:::
## OpenAPI
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details.
+18 -17
View File
@@ -32,6 +32,10 @@ This environment includes the services below. Additional details are available i
All the services are packaged to run as with single Docker Compose command.
:::tip mise
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
:::
### Server and web apps
1. Clone the project repo.
@@ -56,22 +60,23 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
#### Connect web to a remote backend
If you only want to do web development connected to an existing, remote backend, follow these steps:
1. Build the Immich SDK - `pnpm --filter @immich/sdk install && pnpm --filter @immich/sdk build`
2. Enter the web directory - `cd web/`
3. Install web dependencies - `pnpm i`
4. Start the web development server
If you only want to do web development connected to an existing, remote backend, run from the repo root:
```bash
IMMICH_SERVER_URL=https://demo.immich.app/ pnpm run dev
IMMICH_SERVER_URL=https://demo.immich.app/ mise //web:start
```
This will install all dependencies (including the SDK) and start the dev server in one step. To connect to the hosted demo server specifically, use the shorthand:
```bash
mise //web:start-demo
```
If you're using PowerShell on Windows you may need to set the env var separately like so:
```powershell
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
pnpm run dev
mise //web:start
```
#### `@immich/ui`
@@ -90,20 +95,16 @@ To see local changes to `@immich/ui` in Immich, do the following:
#### Setup
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
2. Change to the immich (root) directory and trust the mise config with `mise trust`.
3. Install tools with mise: `mise install`.
4. Change to the `mobile/` directory.
5. Run `flutter pub get` to install the dependencies.
6. Run `make translation` to generate the translation file.
7. Run `flutter run` to start the app.
1. Run `mise //mobile:install` to install Flutter dependencies.
2. Run `mise //mobile:translation` to generate the translation file.
3. Change to the `mobile/` directory and run `flutter run` to start the app.
#### Translation
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then, from the `mobile/` directory, run
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then run:
```bash
make translation
mise //mobile:translation
```
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
+3 -4
View File
@@ -4,8 +4,8 @@
### Unit tests
Unit are run by calling `pnpm run test` from the `server/` directory.
You need to run `pnpm install` (in `server/`) before _once_.
Unit tests are run with `mise //server:test`.
You need to run `mise //server:install` before _once_.
### End to end tests
@@ -17,8 +17,7 @@ make e2e
Before you can run the tests, you need to run the following commands _once_:
- `pnpm install`
- `pnpm --filter @immich/sdk --filter @immich/cli build`
- `mise //e2e:ci-setup` (installs e2e, SDK, and CLI dependencies)
- `mise //:open-api`
Once the test environment is running, the e2e tests can be run via:
+27 -27
View File
@@ -154,33 +154,33 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Containers |
| :---------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__OCR__RECOGNITION` | Comma-separated list of (recognition) OCR model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__OCR__DETECTION` | Comma-separated list of (detection) OCR model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spun up while inferencing. | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
| `MACHINE_LEARNING_OPENVINO_PRECISION` | If set to FP16, uses half-precision floating-point operations for faster inference with reduced accuracy (one of [`FP16`, `FP32`], applies only to OpenVINO) | `FP32` | machine learning |
| Variable | Description | Default | Containers |
| :---------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `300` (`900` if using ROCm) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__OCR__RECOGNITION` | Comma-separated list of (recognition) OCR model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__OCR__DETECTION` | Comma-separated list of (detection) OCR model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spun up while inferencing. | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
| `MACHINE_LEARNING_OPENVINO_PRECISION` | If set to FP16, uses half-precision floating-point operations for faster inference with reduced accuracy (one of [`FP16`, `FP32`], applies only to OpenVINO) | `FP32` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
+1 -1
View File
@@ -3,7 +3,7 @@ run = "pnpm install --filter documentation --frozen-lockfile"
[tasks.start]
env._.path = "./node_modules/.bin"
run = "docusaurus --port 3005"
run = "docusaurus start --port 3005"
[tasks.build]
env._.path = "./node_modules/.bin"
+1 -3
View File
@@ -83,9 +83,7 @@ volumes:
model_cache:
prometheus_data:
grafana_data:
pnpm_cache:
pnpm_store_server:
pnpm_store_web:
build_cache:
server_node_modules:
web_node_modules:
github_node_modules:
+1 -1
View File
@@ -44,7 +44,7 @@ services:
redis:
container_name: immich-e2e-redis
image: docker.io/valkey/valkey:9@sha256:4963247afc4cd33c7d3b2d2816b9f7f8eeebab148d29056c2ca4d7cbc966f2d9
image: docker.io/valkey/valkey:9@sha256:8436e10bc65c94886a91d4415b6a6dfa9cb5a306fb3b996e5bb67cd2b4854193
healthcheck:
test: redis-cli ping || exit 1
+16 -1
View File
@@ -1,11 +1,21 @@
[tasks.install]
run = "pnpm install --filter immich-e2e --frozen-lockfile"
[tasks.build]
dir = "{{ config_root }}"
run = "docker compose build"
[tasks.test]
depends = ["//e2e:build", "//e2e:ci-setup"]
env._.path = "./node_modules/.bin"
run = "vitest --run"
[tasks.playwright-install]
env._.path = "./node_modules/.bin"
run = "playwright install"
[tasks."test-web"]
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
env._.path = "./node_modules/.bin"
run = "playwright test"
@@ -30,7 +40,12 @@ run = "tsc --noEmit"
[tasks.ci-setup]
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
depends = [
"//:sdk:install",
"//:sdk:build",
"//packages/cli:install",
"//packages/cli:build",
]
run = { task = ":install" }
@@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
result.duration.push(asset.duration);
result.projectionType.push(asset.projectionType);
result.livePhotoVideoId.push(asset.livePhotoVideoId);
result.city.push(asset.city);
result.country.push(asset.country);
result.city?.push(asset.city);
result.country?.push(asset.country);
result.visibility.push(asset.visibility);
}
@@ -536,7 +536,7 @@ test.describe('Timeline', () => {
force: false,
ids: [assetToTrash.id],
});
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
await page.locator('#control-bar').getByLabel('Close').click();
await page.getByText('Trash', { exact: true }).click();
await timelineUtils.waitForTimelineLoad(page);
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
@@ -676,7 +676,7 @@ test.describe('Timeline', () => {
ids: [assetToArchive.id],
});
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
await page.locator('#control-bar').getByLabel('Close').click();
await page.getByRole('link').getByText('Archive').click();
await timelineUtils.waitForTimelineLoad(page);
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
@@ -823,7 +823,7 @@ test.describe('Timeline', () => {
});
// ensure thumbnail still exists and has favorite icon
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
await page.locator('#control-bar').getByLabel('Close').click();
await page.getByRole('link').getByText('Favorites').click();
await timelineUtils.waitForTimelineLoad(page);
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
+10
View File
@@ -698,6 +698,7 @@
"birthdate_saved": "Date of birth saved successfully",
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
"blurred_background": "Blurred background",
"browse_templates": "Browse templates",
"bugs_and_feature_requests": "Bugs & Feature Requests",
"build": "Build",
"build_image": "Build Image",
@@ -839,6 +840,7 @@
"copy_error": "Copy error",
"copy_file_path": "Copy file path",
"copy_image": "Copy Image",
"copy_json": "Copy JSON",
"copy_link": "Copy link",
"copy_link_to_clipboard": "Copy link to clipboard",
"copy_password": "Copy password",
@@ -976,7 +978,10 @@
"downloading_asset_filename": "Downloading asset {filename}",
"downloading_from_icloud": "Downloading from iCloud",
"downloading_media": "Downloading media",
"drag_to_reorder": "Drag to reorder",
"drop_files_to_upload": "Drop files anywhere to upload",
"duplicate": "Duplicate",
"duplicate_workflow": "Duplicate workflow",
"duplicates": "Duplicates",
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
"duration": "Duration",
@@ -2228,6 +2233,7 @@
"slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings",
"smart_album": "Smart album",
"sort_albums_by": "Sort albums by...",
"sort_created": "Date created",
"sort_items": "Number of items",
@@ -2254,6 +2260,7 @@
"step_delete_confirm": "Are you sure you want to delete this step?",
"step_details": "Step details",
"steps": "Steps",
"steps_count": "{count, plural, one {# step} other {# steps}}",
"stop_casting": "Stop casting",
"stop_motion_photo": "Stop Motion Photo",
"stop_photo_sharing": "Stop sharing your photos?",
@@ -2415,6 +2422,7 @@
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
"use_current_connection": "Use current connection",
"use_custom_date_range": "Use custom date range instead",
"use_template": "Use template",
"user": "User",
"user_has_been_deleted": "This user has been deleted.",
"user_id": "User ID",
@@ -2476,6 +2484,7 @@
"week": "Week",
"welcome": "Welcome",
"welcome_to_immich": "Welcome to Immich",
"when": "When",
"width": "Width",
"wifi_name": "Wi-Fi Name",
"workflow": "Workflow",
@@ -2488,6 +2497,7 @@
"workflow_name": "Workflow name",
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
"workflow_summary": "Workflow summary",
"workflow_templates": "Workflow templates",
"workflow_update_success": "Workflow updated successfully",
"workflow_updated": "Workflow updated",
"workflows": "Workflows",
+6 -2
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from socket import socket
from gunicorn.arbiter import Arbiter
from pydantic import BaseModel
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from rich.console import Console
from rich.logging import RichHandler
@@ -42,6 +42,10 @@ class MaxBatchSize(BaseModel):
ocr: int | None = None
def default_worker_timeout() -> int:
return 900 if os.environ.get("DEVICE") == "rocm" else 300
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="MACHINE_LEARNING_",
@@ -54,7 +58,7 @@ class Settings(BaseSettings):
model_ttl: int = 300
model_ttl_poll_s: int = 10
workers: int = 1
worker_timeout: int = 300
worker_timeout: int = Field(default_factory=default_worker_timeout)
http_keepalive_timeout_s: int = 2
test_full: bool = False
request_threads: int = os.cpu_count() or 4
@@ -89,4 +89,10 @@ class FaceRecognizer(InferenceModel):
@property
def _batch_size_default(self) -> int | None:
providers = ort.get_available_providers()
return None if self.model_format == ModelFormat.ONNX and "OpenVINOExecutionProvider" not in providers else 1
if (
self.model_format == ModelFormat.ONNX
and "MIGraphXExecutionProvider" not in providers
and "OpenVINOExecutionProvider" not in providers
):
return None
return 1
@@ -64,6 +64,7 @@ class TextRecognizer(InferenceModel):
rec_batch_num=max_batch_size if max_batch_size else 6,
rec_img_shape=(3, 48, 320),
lang_type=self.language,
model_root_dir=self.cache_dir,
)
)
return session
+47 -1
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from threading import Lock
from typing import Any
import numpy as np
@@ -12,6 +13,37 @@ from immich_ml.schemas import ModelPrecision, SessionNode
from ..config import log, settings
MigraphxInputSignature = tuple[tuple[str, str, tuple[int, ...]], ...]
_migraphx_registry_lock = Lock()
_migraphx_model_locks: dict[str, Lock] = {}
_migraphx_compiled_inputs: set[tuple[str, MigraphxInputSignature]] = set()
def _migraphx_get_model_lock(model_key: str) -> Lock:
with _migraphx_registry_lock:
lock = _migraphx_model_locks.get(model_key)
if lock is None:
lock = Lock()
_migraphx_model_locks[model_key] = lock
return lock
def _migraphx_has_compiled_input(key: tuple[str, MigraphxInputSignature]) -> bool:
with _migraphx_registry_lock:
return key in _migraphx_compiled_inputs
def _migraphx_mark_compiled_input(key: tuple[str, MigraphxInputSignature]) -> None:
with _migraphx_registry_lock:
_migraphx_compiled_inputs.add(key)
def _migraphx_input_signature(
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
) -> MigraphxInputSignature:
return tuple((name, str(value.dtype), tuple(value.shape)) for name, value in sorted(input_feed.items()))
class OrtSession:
session: ort.InferenceSession
@@ -48,7 +80,21 @@ class OrtSession:
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
run_options: Any = None,
) -> list[NDArray[np.float32]]:
outputs: list[NDArray[np.float32]] = self.session.run(output_names, input_feed, run_options)
if "MIGraphXExecutionProvider" in self.providers:
model_key = self.model_path.resolve().as_posix()
input_key = (model_key, _migraphx_input_signature(input_feed))
if not _migraphx_has_compiled_input(input_key):
model_lock = _migraphx_get_model_lock(model_key)
with model_lock:
if not _migraphx_has_compiled_input(input_key):
outputs: list[NDArray[np.float32]] = self.session.run(output_names, input_feed, run_options)
_migraphx_mark_compiled_input(input_key)
return outputs
outputs = self.session.run(output_names, input_feed, run_options)
return outputs
outputs = self.session.run(output_names, input_feed, run_options)
return outputs
@property
+122 -3
View File
@@ -35,7 +35,37 @@ from immich_ml.sessions.ort import OrtSession
from immich_ml.sessions.rknn import RknnSession, run_inference
class FakeLock:
def __init__(self) -> None:
self.enter = mock.Mock()
self.exit = mock.Mock()
def __enter__(self) -> None:
self.enter()
def __exit__(self, *args: object) -> None:
self.exit(*args)
class TestBase:
def test_sets_default_worker_timeout(self, monkeypatch: MonkeyPatch) -> None:
monkeypatch.delenv("DEVICE", raising=False)
monkeypatch.delenv("MACHINE_LEARNING_WORKER_TIMEOUT", raising=False)
assert Settings().worker_timeout == 300
def test_sets_rocm_default_worker_timeout(self, monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("DEVICE", "rocm")
monkeypatch.delenv("MACHINE_LEARNING_WORKER_TIMEOUT", raising=False)
assert Settings().worker_timeout == 900
def test_worker_timeout_env_override(self, monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("DEVICE", "rocm")
monkeypatch.setenv("MACHINE_LEARNING_WORKER_TIMEOUT", "1200")
assert Settings().worker_timeout == 1200
def test_sets_default_cache_dir(self) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
@@ -413,6 +443,52 @@ class TestOrtSession:
assert sess_options is session.sess_options
def test_serializes_rocm_first_run_for_new_input_signature(self, mocker: MockerFixture) -> None:
lock = FakeLock()
get_model_lock = mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
mocker.patch("immich_ml.sessions.ort._migraphx_compiled_inputs", set())
mocker.patch("immich_ml.sessions.ort.Path.mkdir")
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["MIGraphXExecutionProvider"])
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
session.run(None, input_feed)
session.run(None, input_feed)
lock.enter.assert_called_once()
lock.exit.assert_called_once()
get_model_lock.assert_called_once()
session.session.run.assert_has_calls([mock.call(None, input_feed, None), mock.call(None, input_feed, None)])
def test_serializes_rocm_run_for_each_new_input_signature(self, mocker: MockerFixture) -> None:
lock = FakeLock()
mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
mocker.patch("immich_ml.sessions.ort._migraphx_compiled_inputs", set())
mocker.patch("immich_ml.sessions.ort.Path.mkdir")
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["MIGraphXExecutionProvider"])
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
new_shape_input_feed = {"input": np.random.rand(2, 3, 224, 224).astype(np.float32)}
session.run(None, input_feed)
session.run(None, new_shape_input_feed)
assert lock.enter.call_count == 2
assert lock.exit.call_count == 2
session.session.run.assert_has_calls(
[mock.call(None, input_feed, None), mock.call(None, new_shape_input_feed, None)]
)
def test_does_not_serialize_non_rocm_run(self, mocker: MockerFixture) -> None:
lock = FakeLock()
get_model_lock = mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["CPUExecutionProvider"])
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
session.run(None, input_feed)
get_model_lock.assert_not_called()
lock.enter.assert_not_called()
session.session.run.assert_called_once_with(None, input_feed, None)
class TestAnnSession:
def test_creates_ann_session(self, ann_session: mock.Mock, info: mock.Mock) -> None:
@@ -883,6 +959,34 @@ class TestFaceRecognition:
onnx.load.assert_not_called()
onnx.save.assert_not_called()
def test_recognition_does_not_add_batch_axis_for_migraphx(
self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
) -> None:
onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch(
"immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
)
mocker.patch("immich_ml.models.base.InferenceModel.download")
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
mocker.patch(
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
return_value=["MIGraphXExecutionProvider", "CPUExecutionProvider"],
)
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
inputs = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
outputs = [SimpleNamespace(name="output.1", shape=(1, 800))]
ort_session.return_value.get_inputs.return_value = inputs
ort_session.return_value.get_outputs.return_value = outputs
face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
face_recognizer.load()
assert face_recognizer.batch_size == 1
update_dims.assert_not_called()
onnx.load.assert_not_called()
onnx.save.assert_not_called()
def test_set_custom_max_batch_size(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(facial_recognition=2))
@@ -924,7 +1028,12 @@ class TestOcr:
text_recognizer.load()
rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
OcrOptions(
session=ort_session.return_value,
rec_batch_num=6,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
)
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
@@ -937,7 +1046,12 @@ class TestOcr:
text_recognizer.load()
rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320))
OcrOptions(
session=ort_session.return_value,
rec_batch_num=4,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
)
def test_ignore_other_custom_max_batch_size(
@@ -952,7 +1066,12 @@ class TestOcr:
text_recognizer.load()
rapid_recognizer.assert_called_once_with(
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
OcrOptions(
session=ort_session.return_value,
rec_batch_num=6,
rec_img_shape=(3, 48, 320),
model_root_dir=text_recognizer.cache_dir,
)
)
+23 -1
View File
@@ -1,9 +1,31 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.41.9"
version = "3.44.0"
backend = "aqua:flutter/flutter"
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
[[tools.flutter]]
version = "3.41.9-stable"
backend = "asdf:flutter"
+80 -4
View File
@@ -16,7 +16,7 @@ config_roots = [
[tools]
node = "24.15.0"
"aqua:flutter/flutter" = "3.41.9"
"aqua:flutter/flutter" = "3.44.0"
pnpm = "10.33.4"
terragrunt = "1.0.3"
opentofu = "1.11.6"
@@ -54,8 +54,8 @@ lockfile = true
[tasks.plugins]
run = [
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build",
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build",
]
[tasks.open-api-typescript]
@@ -73,7 +73,6 @@ run = "bash ./bin/generate-dart-sdk.sh"
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
run = [
{ task = "//:plugins" },
{ task = "//server:build" },
{ task = "//server:install" },
{ task = "//server:build" },
{ task = "//server:sync-open-api" },
@@ -85,6 +84,72 @@ run = [
dir = "server"
run = "node ./dist/bin/sync-sql.js"
# TODO dev, prod, and e2e should be de-duplicated by using env but for some reason I ran into issues
[tasks.dev]
depends = "//:plugins"
dir = "docker"
interactive = true
env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
depends_post = "//:dev-down"
[tasks.dev-update]
run = { task = "//:dev", args = ["--build", "-V"] }
[tasks.dev-scale]
run = { task = "//:dev", args = ["--build", "-V", "--scale immich-server=3"] }
[tasks.dev-down]
dir = "docker"
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
[tasks.prod]
depends = "//:plugins"
dir = "docker"
interactive = true
env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans"
depends_post = "//:prod-down"
[tasks.prod-scale]
run = { task = "//:prod", args = [
"--build",
"-V",
"--scale immich-server=3",
"--scale immich-microservices",
] }
[tasks.prod-down]
dir = "docker"
run = "docker compose -f ./docker-compose.prod.yml down --remove-orphans"
[tasks.e2e]
depends = "//:plugins"
dir = "e2e"
interactive = true
env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.yml up --remove-orphans"
depends_post = "//:e2e-down"
[tasks.e2e-dev]
depends = "//:plugins"
dir = "e2e"
interactive = true
env = { COMPOSE_BAKE = true }
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
depends_post = "//:e2e-dev-down"
[tasks.e2e-update]
run = { task = "//:e2e", args = ["--build", '-V'] }
[tasks.e2e-down]
dir = "e2e"
run = "docker compose -f ./docker-compose.yml down --remove-orphans"
[tasks.e2e-dev-down]
dir = "e2e"
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
# SDK tasks
[tasks."sdk:install"]
dir = "packages/sdk"
@@ -100,3 +165,14 @@ run = "pnpm format"
[tasks."i18n:format-fix"]
run = "pnpm format:fix"
[tasks.clean]
run = [
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
{ task = "//:*-down" },
]
-1
View File
@@ -1,5 +1,4 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
+4
View File
@@ -5,3 +5,7 @@ android.nonTransitiveRClass=false
android.nonFinalResIds=false
org.gradle.caching=true
org.gradle.parallel=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
File diff suppressed because it is too large Load Diff
-123
View File
@@ -1,58 +1,23 @@
PODS:
- background_downloader (0.0.1):
- Flutter
- bonsoir_darwin (0.0.1):
- Flutter
- FlutterMacOS
- connectivity_plus (0.0.1):
- Flutter
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_udid (0.0.1):
- Flutter
- KeychainAccess
- flutter_web_auth_2 (5.0.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- KeychainAccess (4.2.2)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
- MapLibre (6.14.0)
- maplibre_gl (0.0.1):
- Flutter
- MapLibre (= 6.14.0)
- native_video_player (1.0.0):
- Flutter
- network_info_plus (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@@ -61,144 +26,56 @@ PODS:
- Flutter
- share_handler_ios_models
- share_handler_ios_models (0.0.9)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
- Flutter
DEPENDENCIES:
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- KeychainAccess
- MapLibre
EXTERNAL SOURCES:
background_downloader:
:path: ".symlinks/plugins/background_downloader/ios"
bonsoir_darwin:
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_udid:
:path: ".symlinks/plugins/flutter_udid/ios"
flutter_web_auth_2:
:path: ".symlinks/plugins/flutter_web_auth_2/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
maplibre_gl:
:path: ".symlinks/plugins/maplibre_gl/ios"
native_video_player:
:path: ".symlinks/plugins/native_video_player/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/darwin"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
SPEC CHECKSUMS:
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
@@ -39,6 +39,7 @@
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -129,6 +130,7 @@
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = "<group>"; };
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -193,6 +195,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
@@ -247,6 +250,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -360,6 +364,9 @@
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
@@ -463,6 +470,7 @@
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
);
@@ -1285,7 +1293,17 @@
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
productName = StructuredFieldValues;
};
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -1,5 +1,4 @@
{
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -19,6 +18,24 @@
"version" : "7.8.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
"version" : "6.14.0"
}
},
{
"identity" : "sqlite-data",
"kind" : "remoteSourceControl",
@@ -146,5 +163,5 @@
}
}
],
"version" : 3
"version" : 2
}
@@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
@@ -1,5 +1,4 @@
{
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
"pins" : [
{
"identity" : "combine-schedulers",
@@ -19,6 +18,24 @@
"version" : "7.9.0"
}
},
{
"identity" : "keychainaccess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
"state" : {
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
"version" : "4.2.2"
}
},
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
"state" : {
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
"version" : "6.14.0"
}
},
{
"identity" : "sqlite-data",
"kind" : "remoteSourceControl",
@@ -146,5 +163,5 @@
}
}
],
"version" : 3
"version" : 2
}
+1
View File
@@ -23,6 +23,7 @@ const String kBackupLivePhotoGroup = 'backup_live_photo_group';
const String kDownloadGroupImage = 'group_image';
const String kDownloadGroupVideo = 'group_video';
const String kDownloadGroupLivePhoto = 'group_livephoto';
const String kShareDownloadGroup = 'group_share';
// Timeline constants
const int kTimelineNoneSegmentSize = 120;
+123 -3
View File
@@ -1,14 +1,25 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/album_config.dart';
import 'package:immich_mobile/domain/models/config/backup_config.dart';
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
import 'package:immich_mobile/domain/models/config/image_config.dart';
import 'package:immich_mobile/domain/models/config/map_config.dart';
import 'package:immich_mobile/domain/models/config/network_config.dart';
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
import 'package:immich_mobile/domain/models/config/theme_config.dart';
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
const defaultConfig = AppConfig();
class AppConfig {
final LogLevel logLevel;
final ThemeConfig theme;
final CleanupConfig cleanup;
final MapConfig map;
@@ -18,8 +29,10 @@ class AppConfig {
final SlideshowConfig slideshow;
final AlbumConfig album;
final BackupConfig backup;
final NetworkConfig network;
const AppConfig({
this.logLevel = .info,
this.theme = const .new(),
this.cleanup = const .new(),
this.map = const .new(),
@@ -29,9 +42,11 @@ class AppConfig {
this.slideshow = const .new(),
this.album = const .new(),
this.backup = const .new(),
this.network = const .new(),
});
AppConfig copyWith({
LogLevel? logLevel,
ThemeConfig? theme,
CleanupConfig? cleanup,
MapConfig? map,
@@ -41,7 +56,9 @@ class AppConfig {
SlideshowConfig? slideshow,
AlbumConfig? album,
BackupConfig? backup,
NetworkConfig? network,
}) => .new(
logLevel: logLevel ?? this.logLevel,
theme: theme ?? this.theme,
cleanup: cleanup ?? this.cleanup,
map: map ?? this.map,
@@ -51,12 +68,14 @@ class AppConfig {
slideshow: slideshow ?? this.slideshow,
album: album ?? this.album,
backup: backup ?? this.backup,
network: network ?? this.network,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is AppConfig &&
other.logLevel == logLevel &&
other.theme == theme &&
other.cleanup == cleanup &&
other.map == map &&
@@ -65,12 +84,113 @@ class AppConfig {
other.viewer == viewer &&
other.slideshow == slideshow &&
other.album == album &&
other.backup == backup);
other.backup == backup &&
other.network == network);
@override
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup);
int get hashCode =>
Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network);
@override
String toString() =>
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)';
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)';
T read<T extends Object>(SettingsKey<T> key) =>
(switch (key) {
.logLevel => logLevel,
.themePrimaryColor => theme.primaryColor,
.themeMode => theme.mode,
.themeDynamic => theme.dynamicTheme,
.themeColorfulInterface => theme.colorfulInterface,
.imagePreferRemote => image.preferRemote,
.imageLoadOriginal => image.loadOriginal,
.viewerLoopVideo => viewer.loopVideo,
.viewerLoadOriginalVideo => viewer.loadOriginalVideo,
.viewerAutoPlayVideo => viewer.autoPlayVideo,
.viewerTapToNavigate => viewer.tapToNavigate,
.networkAutoEndpointSwitching => network.autoEndpointSwitching,
.networkPreferredWifiName => network.preferredWifiName,
.networkLocalEndpoint => network.localEndpoint,
.networkExternalEndpointList => network.externalEndpointList,
.networkCustomHeaders => network.customHeaders,
.albumSortMode => album.sortMode,
.albumIsReverse => album.isReverse,
.albumIsGrid => album.isGrid,
.backupEnabled => backup.enabled,
.backupUseCellularForVideos => backup.useCellularForVideos,
.backupUseCellularForPhotos => backup.useCellularForPhotos,
.backupRequireCharging => backup.requireCharging,
.backupTriggerDelay => backup.triggerDelay,
.backupSyncAlbums => backup.syncAlbums,
.timelineTilesPerRow => timeline.tilesPerRow,
.timelineGroupAssetsBy => timeline.groupAssetsBy,
.timelineStorageIndicator => timeline.storageIndicator,
.mapShowFavoriteOnly => map.favoritesOnly,
.mapRelativeDate => map.relativeDays,
.mapIncludeArchived => map.includeArchived,
.mapThemeMode => map.themeMode,
.mapWithPartners => map.withPartners,
.cleanupKeepFavorites => cleanup.keepFavorites,
.cleanupKeepMediaType => cleanup.keepMediaType,
.cleanupKeepAlbumIds => cleanup.keepAlbumIds,
.cleanupCutoffDaysAgo => cleanup.cutoffDaysAgo,
.cleanupDefaultsInitialized => cleanup.defaultsInitialized,
.slideshowTransition => slideshow.transition,
.slideshowRepeat => slideshow.repeat,
.slideshowDuration => slideshow.duration,
.slideshowLook => slideshow.look,
.slideshowDirection => slideshow.direction,
})
as T;
factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) =>
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
AppConfig write<T extends Object>(SettingsKey<T> key, T value) {
return switch (key) {
.logLevel => copyWith(logLevel: value as LogLevel),
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
.themeMode => copyWith(theme: theme.copyWith(mode: value as ThemeMode)),
.themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)),
.themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)),
.imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)),
.imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)),
.viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)),
.viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)),
.viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)),
.viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)),
.networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)),
.networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))),
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))),
.networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List<String>)),
.networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map<String, String>)),
.albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)),
.albumIsReverse => copyWith(album: album.copyWith(isReverse: value as bool)),
.albumIsGrid => copyWith(album: album.copyWith(isGrid: value as bool)),
.backupEnabled => copyWith(backup: backup.copyWith(enabled: value as bool)),
.backupUseCellularForVideos => copyWith(backup: backup.copyWith(useCellularForVideos: value as bool)),
.backupUseCellularForPhotos => copyWith(backup: backup.copyWith(useCellularForPhotos: value as bool)),
.backupRequireCharging => copyWith(backup: backup.copyWith(requireCharging: value as bool)),
.backupTriggerDelay => copyWith(backup: backup.copyWith(triggerDelay: value as int)),
.backupSyncAlbums => copyWith(backup: backup.copyWith(syncAlbums: value as bool)),
.timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)),
.timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)),
.timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)),
.mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)),
.mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)),
.mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)),
.mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)),
.mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)),
.cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)),
.cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)),
.cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List<String>)),
.cleanupCutoffDaysAgo => copyWith(cleanup: cleanup.copyWith(cutoffDaysAgo: value as int)),
.cleanupDefaultsInitialized => copyWith(cleanup: cleanup.copyWith(defaultsInitialized: value as bool)),
.slideshowTransition => copyWith(slideshow: slideshow.copyWith(transition: value as bool)),
.slideshowRepeat => copyWith(slideshow: slideshow.copyWith(repeat: value as bool)),
.slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)),
.slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)),
.slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)),
};
}
}
@@ -2,15 +2,15 @@ import 'package:flutter/foundation.dart';
class NetworkConfig {
final bool autoEndpointSwitching;
final String? preferredWifiName;
final String? localEndpoint;
final String preferredWifiName;
final String localEndpoint;
final List<String> externalEndpointList;
final Map<String, String> customHeaders;
const NetworkConfig({
this.autoEndpointSwitching = false,
this.preferredWifiName,
this.localEndpoint,
this.preferredWifiName = '',
this.localEndpoint = '',
this.externalEndpointList = const [],
this.customHeaders = const {},
});
@@ -1,22 +0,0 @@
import 'package:immich_mobile/domain/models/config/network_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
class SystemConfig {
final LogLevel logLevel;
final NetworkConfig network;
const SystemConfig({this.logLevel = .info, this.network = const .new()});
SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) =>
SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network);
@override
bool operator ==(Object other) =>
identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network);
@override
int get hashCode => Object.hash(logLevel, network);
@override
String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)';
}
-273
View File
@@ -1,273 +0,0 @@
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum MetadataDomain<T extends Object> {
appConfig<AppConfig>('config.app'),
systemConfig<SystemConfig>('config.system');
final String prefix;
const MetadataDomain(this.prefix);
}
enum MetadataKey<T extends Object> {
// Theme
themePrimaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
// Image
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
// Viewer
viewerLoopVideo<bool>(.appConfig, 'viewer.loopVideo', true),
viewerLoadOriginalVideo<bool>(.appConfig, 'viewer.loadOriginalVideo', false),
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
// Network
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
networkExternalEndpointList<List<String>>(
.systemConfig,
'network.externalEndpointList',
[],
_ListCodec(_PrimitiveCodec.string),
),
networkCustomHeaders<Map<String, String>>(
.systemConfig,
'network.customHeaders',
{},
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
),
// Album
albumSortMode<AlbumSortMode>(
.appConfig,
'album.sortMode',
AlbumSortMode.mostRecent,
_EnumCodec(AlbumSortMode.values),
),
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
// Backup
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
// Timeline
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
timelineGroupAssetsBy<GroupAssetsBy>(
.appConfig,
'timeline.groupAssetsBy',
GroupAssetsBy.day,
_EnumCodec(GroupAssetsBy.values),
),
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
// 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>(
.appConfig,
'cleanup.keepMediaType',
AssetKeepType.none,
_EnumCodec(AssetKeepType.values),
),
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false),
// Slideshow
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(
.appConfig,
'slideshow.direction',
SlideshowDirection.forward,
_EnumCodec(SlideshowDirection.values),
);
final MetadataDomain domain;
final String name;
final T defaultValue;
final _MetadataCodec<T>? _codecOverride;
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
String get key => '${domain.prefix}.$name';
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
}
sealed class _MetadataCodec<T extends Object> {
const _MetadataCodec();
String encode(T value);
T? decode(String raw);
static const Map<Type, _MetadataCodec<Object>> _primitives = {
int: _PrimitiveCodec.integer,
double: _PrimitiveCodec.real,
bool: _PrimitiveCodec.boolean,
String: _PrimitiveCodec.string,
DateTime: _DateTimeCodec(),
};
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
final codec = _primitives[sample.runtimeType];
if (codec == null) {
throw StateError(
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
);
}
return codec as _MetadataCodec<T>;
}
}
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
final List<T> values;
const _EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
}
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
const _DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime? decode(String raw) => DateTime.tryParse(raw);
}
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
final _MetadataCodec<K> _keyCodec;
final _MetadataCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return null;
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
return null;
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
if (k == null || v == null) {
return null;
}
result[k] = v;
}
return result;
} on FormatException {
return null;
}
}
}
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
final _MetadataCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T>? decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return null;
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return null;
}
final element = _elementCodec.decode(item);
if (element == null) {
return null;
}
result.add(element);
}
return result;
} on FormatException {
return null;
}
}
}
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
final T? Function(String) _parse;
const _PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T? decode(String raw) => _parse(raw);
static const integer = _PrimitiveCodec<int>._(int.tryParse);
static const real = _PrimitiveCodec<double>._(double.tryParse);
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
static const string = _PrimitiveCodec<String>._(_identity);
static String? _identity(String s) => s;
}
+217
View File
@@ -0,0 +1,217 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
enum SettingsKey<T extends Object> {
// Theme
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
themeDynamic<bool>(),
themeColorfulInterface<bool>(),
// Image
imagePreferRemote<bool>(),
imageLoadOriginal<bool>(),
// Viewer
viewerLoopVideo<bool>(),
viewerLoadOriginalVideo<bool>(),
viewerAutoPlayVideo<bool>(),
viewerTapToNavigate<bool>(),
// Network
networkAutoEndpointSwitching<bool>(),
networkPreferredWifiName<String>(),
networkLocalEndpoint<String>(),
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
// Album
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
albumIsReverse<bool>(),
albumIsGrid<bool>(),
// Backup
backupEnabled<bool>(),
backupUseCellularForVideos<bool>(),
backupUseCellularForPhotos<bool>(),
backupRequireCharging<bool>(),
backupTriggerDelay<int>(),
backupSyncAlbums<bool>(),
// Timeline
timelineTilesPerRow<int>(),
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
timelineStorageIndicator<bool>(),
// Log
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
// Map
mapShowFavoriteOnly<bool>(),
mapRelativeDate<int>(),
mapIncludeArchived<bool>(),
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
mapWithPartners<bool>(),
// Cleanup
cleanupKeepFavorites<bool>(),
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
cleanupCutoffDaysAgo<int>(),
cleanupDefaultsInitialized<bool>(),
// Slideshow
slideshowTransition<bool>(),
slideshowRepeat<bool>(),
slideshowDuration<int>(),
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
final _SettingsCodec<T>? _codecOverride;
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
String encode(T value) => _codec.encode(value);
T decode(String raw) => _codec.decode(raw);
}
sealed class _SettingsCodec<T extends Object> {
const _SettingsCodec();
String encode(T value);
T decode(String raw);
static const Map<Type, _SettingsCodec<Object>> _primitives = {
int: _PrimitiveCodec.integer,
double: _PrimitiveCodec.real,
bool: _PrimitiveCodec.boolean,
String: _PrimitiveCodec.string,
DateTime: _DateTimeCodec(),
};
static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
final codec = _primitives[runtimeType];
if (codec == null) {
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
}
return codec as _SettingsCodec<T>;
}
}
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
final List<T> values;
const _EnumCodec(this.values);
@override
String encode(T value) => value.name;
@override
T decode(String raw) => values.firstWhere((v) => v.name == raw);
}
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
const _DateTimeCodec();
@override
String encode(DateTime value) => value.toIso8601String();
@override
DateTime decode(String raw) => DateTime.parse(raw);
}
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
final _SettingsCodec<K> _keyCodec;
final _SettingsCodec<V> _valueCodec;
const _MapCodec(this._keyCodec, this._valueCodec);
@override
String encode(Map<K, V> value) {
final entries = <String, String>{};
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
return jsonEncode(entries);
}
@override
Map<K, V> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! Map) {
return {};
}
final result = <K, V>{};
for (final entry in decoded.entries) {
final rawKey = entry.key;
final rawValue = entry.value;
if (rawKey is! String || rawValue is! String) {
return {};
}
final k = _keyCodec.decode(rawKey);
final v = _valueCodec.decode(rawValue);
result[k] = v;
}
return result;
} on FormatException {
return {};
}
}
}
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
final _SettingsCodec<T> _elementCodec;
const _ListCodec(this._elementCodec);
@override
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
@override
List<T> decode(String raw) {
try {
final decoded = jsonDecode(raw);
if (decoded is! List) {
return [];
}
final result = <T>[];
for (final item in decoded) {
if (item is! String) {
return [];
}
final element = _elementCodec.decode(item);
result.add(element);
}
return result;
} on FormatException {
return [];
}
}
}
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
final T Function(String) _parse;
const _PrimitiveCodec._(this._parse);
@override
String encode(T value) => value.toString();
@override
T decode(String raw) => _parse(raw);
static const integer = _PrimitiveCodec<int>._(int.parse);
static const real = _PrimitiveCodec<double>._(double.parse);
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
static const string = _PrimitiveCodec<String>._(_identity);
static String _identity(String s) => s;
}
@@ -8,11 +8,7 @@ class AssetService {
final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository;
const AssetService({
required RemoteAssetRepository remoteAssetRepository,
required DriftLocalAssetRepository localAssetRepository,
}) : _remoteAssetRepository = remoteAssetRepository,
_localAssetRepository = localAssetRepository;
const AssetService({required this._remoteAssetRepository, required this._localAssetRepository});
Future<BaseAsset?> getAsset(BaseAsset asset) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
@@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
@@ -39,7 +39,7 @@ class BackgroundWorkerFgService {
_foregroundHostApi.saveNotificationMessage(title, body);
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
final backup = MetadataRepository.instance.appConfig.backup;
final backup = SettingsRepository.instance.appConfig.backup;
return _foregroundHostApi.configure(
BackgroundWorkerSettings(
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
@@ -61,15 +61,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
bool _isCleanedUp = false;
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
: _drift = drift,
_driftLogger = driftLogger,
_backgroundHostApi = BackgroundWorkerBgHostApi() {
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
BackgroundWorkerBgService({required this._drift, required this._driftLogger})
: _backgroundHostApi = BackgroundWorkerBgHostApi() {
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(_drift))]);
BackgroundWorkerFlutterApi.setUp(this);
}
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled;
bool get _isBackupEnabled => SettingsRepository.instance.appConfig.backup.enabled;
Future<void> init() async {
try {
+6 -11
View File
@@ -21,18 +21,13 @@ class HashService {
final _log = Logger('HashService');
HashService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required NativeSyncApi nativeSyncApi,
bool Function()? cancelChecker,
required this._localAlbumRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._nativeSyncApi,
this._cancelChecker,
int? batchSize,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_cancelChecker = cancelChecker,
_nativeSyncApi = nativeSyncApi,
_batchSize = batchSize ?? kBatchHashFileLimit;
}) : _batchSize = batchSize ?? kBatchHashFileLimit;
bool get isCancelled => _cancelChecker?.call() ?? false;
@@ -28,18 +28,13 @@ class LocalSyncService {
final Logger _log = Logger("DeviceSyncService");
LocalSyncService({
required DriftLocalAlbumRepository localAlbumRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required AssetMediaRepository assetMediaRepository,
required IPermissionRepository permissionRepository,
required NativeSyncApi nativeSyncApi,
}) : _localAlbumRepository = localAlbumRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_assetMediaRepository = assetMediaRepository,
_permissionRepository = permissionRepository,
_nativeSyncApi = nativeSyncApi;
required this._localAlbumRepository,
required this._localAssetRepository,
required this._nativeSyncApi,
required this._trashedLocalAssetRepository,
required this._assetMediaRepository,
required this._permissionRepository,
});
Future<void> sync({bool full = false}) async {
final Stopwatch stopwatch = Stopwatch()..start();
+11 -11
View File
@@ -2,9 +2,9 @@ import 'dart:async';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
@@ -12,10 +12,10 @@ import 'package:logging/logging.dart';
///
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
/// writes them to a persistent [LogRepository], and manages log levels via
/// [MetadataRepository].
/// [SettingsRepository].
class LogService {
final LogRepository _logRepository;
final MetadataRepository _metadataRepository;
final SettingsRepository _settingsRepository;
final List<LogMessage> _msgBuffer = [];
@@ -38,12 +38,12 @@ class LogService {
static Future<LogService> init({
required LogRepository logRepository,
required MetadataRepository metadataRepository,
required SettingsRepository settingsRepository,
bool shouldBuffer = true,
}) async {
_instance ??= await create(
logRepository: logRepository,
metadataRepository: metadataRepository,
settingsRepository: settingsRepository,
shouldBuffer: shouldBuffer,
);
return _instance!;
@@ -51,17 +51,17 @@ class LogService {
static Future<LogService> create({
required LogRepository logRepository,
required MetadataRepository metadataRepository,
required SettingsRepository settingsRepository,
bool shouldBuffer = true,
}) async {
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
final instance = LogService._(logRepository, settingsRepository, shouldBuffer);
await logRepository.truncate(limit: kLogTruncateLimit);
final level = instance._metadataRepository.systemConfig.logLevel;
final level = instance._settingsRepository.appConfig.logLevel;
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
return instance;
}
LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) {
LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) {
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
}
@@ -91,7 +91,7 @@ class LogService {
}
Future<void> setLogLevel(LogLevel level) async {
await _metadataRepository.write(MetadataKey.logLevel, level);
await _settingsRepository.write(SettingsKey.logLevel, level);
Logger.root.level = level.toLevel();
}
+1 -1
View File
@@ -10,7 +10,7 @@ typedef MapQuery = ({MapMarkerSource markerSource});
class MapFactory {
final DriftMapRepository _mapRepository;
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
const MapFactory({required this._mapRepository});
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
MapService(_mapRepository.remote(ownerIds, options));
@@ -9,7 +9,7 @@ final AppSetting = SettingsService(storeService: StoreService.I);
class SettingsService {
final StoreService _storeService;
const SettingsService({required StoreService storeService}) : _storeService = storeService;
const SettingsService({required this._storeService});
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
@@ -41,24 +41,16 @@ class SyncStreamService {
final bool Function()? _cancelChecker;
SyncStreamService({
required SyncApiRepository syncApiRepository,
required SyncStreamRepository syncStreamRepository,
required DriftLocalAssetRepository localAssetRepository,
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
required AssetMediaRepository assetMediaRepository,
required IPermissionRepository permissionRepository,
required SyncMigrationRepository syncMigrationRepository,
required ApiService api,
bool Function()? cancelChecker,
}) : _syncApiRepository = syncApiRepository,
_syncStreamRepository = syncStreamRepository,
_localAssetRepository = localAssetRepository,
_trashedLocalAssetRepository = trashedLocalAssetRepository,
_assetMediaRepository = assetMediaRepository,
_permissionRepository = permissionRepository,
_syncMigrationRepository = syncMigrationRepository,
_api = api,
_cancelChecker = cancelChecker;
required this._syncApiRepository,
required this._syncStreamRepository,
required this._localAssetRepository,
required this._trashedLocalAssetRepository,
required this._assetMediaRepository,
required this._permissionRepository,
required this._syncMigrationRepository,
required this._api,
this._cancelChecker,
});
bool get isCancelled => _cancelChecker?.call() ?? false;
@@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
@@ -39,16 +39,12 @@ enum TimelineOrigin {
class TimelineFactory {
final DriftTimelineRepository _timelineRepository;
final MetadataRepository _metadataRepository;
final SettingsRepository _settingsRepository;
const TimelineFactory({
required DriftTimelineRepository timelineRepository,
required MetadataRepository metadataRepository,
}) : _timelineRepository = timelineRepository,
_metadataRepository = metadataRepository;
const TimelineFactory({required this._timelineRepository, required this._settingsRepository});
GroupAssetsBy get groupBy {
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
final group = _settingsRepository.appConfig.timeline.groupAssetsBy;
// We do not support auto grouping in the new timeline yet, fallback to day grouping
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
}
@@ -108,12 +104,7 @@ class TimelineService {
TimelineService(TimelineQuery query)
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
TimelineService._({
required TimelineAssetSource assetSource,
required TimelineBucketSource bucketSource,
required this.origin,
}) : _assetSource = assetSource,
_bucketSource = bucketSource {
TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) {
_bucketSubscription = _bucketSource().listen((buckets) {
_mutex.run(() async {
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
+1 -3
View File
@@ -12,9 +12,7 @@ class UserService {
final UserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
: _userApiRepository = userApiRepository,
_storeService = storeService;
UserService({required this._userApiRepository, required this._storeService});
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
@@ -1,8 +1,8 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class MetadataEntity extends Table with DriftDefaultsMixin {
const MetadataEntity();
class SettingsEntity extends Table with DriftDefaultsMixin {
const SettingsEntity();
TextColumn get key => text()();
@@ -14,5 +14,5 @@ class MetadataEntity extends Table with DriftDefaultsMixin {
Set<Column> get primaryKey => {key};
@override
String get tableName => "metadata";
String get tableName => "settings";
}
@@ -1,28 +1,28 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
as i1;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'
as i2;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
typedef $$MetadataEntityTableCreateCompanionBuilder =
i1.MetadataEntityCompanion Function({
typedef $$SettingsEntityTableCreateCompanionBuilder =
i1.SettingsEntityCompanion Function({
required String key,
required String value,
i0.Value<DateTime> updatedAt,
});
typedef $$MetadataEntityTableUpdateCompanionBuilder =
i1.MetadataEntityCompanion Function({
typedef $$SettingsEntityTableUpdateCompanionBuilder =
i1.SettingsEntityCompanion Function({
i0.Value<String> key,
i0.Value<String> value,
i0.Value<DateTime> updatedAt,
});
class $$MetadataEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableFilterComposer({
class $$SettingsEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$SettingsEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
@@ -45,9 +45,9 @@ class $$MetadataEntityTableFilterComposer
);
}
class $$MetadataEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableOrderingComposer({
class $$SettingsEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$SettingsEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
@@ -70,9 +70,9 @@ class $$MetadataEntityTableOrderingComposer
);
}
class $$MetadataEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
$$MetadataEntityTableAnnotationComposer({
class $$SettingsEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
$$SettingsEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
@@ -89,47 +89,47 @@ class $$MetadataEntityTableAnnotationComposer
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$MetadataEntityTableTableManager
class $$SettingsEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData,
i1.$$MetadataEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder,
i1.$SettingsEntityTable,
i1.SettingsEntityData,
i1.$$SettingsEntityTableFilterComposer,
i1.$$SettingsEntityTableOrderingComposer,
i1.$$SettingsEntityTableAnnotationComposer,
$$SettingsEntityTableCreateCompanionBuilder,
$$SettingsEntityTableUpdateCompanionBuilder,
(
i1.MetadataEntityData,
i1.SettingsEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData
i1.$SettingsEntityTable,
i1.SettingsEntityData
>,
),
i1.MetadataEntityData,
i1.SettingsEntityData,
i0.PrefetchHooks Function()
> {
$$MetadataEntityTableTableManager(
$$SettingsEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$MetadataEntityTable table,
i1.$SettingsEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$MetadataEntityTableFilterComposer($db: db, $table: table),
i1.$$SettingsEntityTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table),
i1.$$SettingsEntityTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () => i1
.$$MetadataEntityTableAnnotationComposer($db: db, $table: table),
.$$SettingsEntityTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> key = const i0.Value.absent(),
i0.Value<String> value = const i0.Value.absent(),
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion(
}) => i1.SettingsEntityCompanion(
key: key,
value: value,
updatedAt: updatedAt,
@@ -139,7 +139,7 @@ class $$MetadataEntityTableTableManager
required String key,
required String value,
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
}) => i1.MetadataEntityCompanion.insert(
}) => i1.SettingsEntityCompanion.insert(
key: key,
value: value,
updatedAt: updatedAt,
@@ -152,34 +152,34 @@ class $$MetadataEntityTableTableManager
);
}
typedef $$MetadataEntityTableProcessedTableManager =
typedef $$SettingsEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData,
i1.$$MetadataEntityTableFilterComposer,
i1.$$MetadataEntityTableOrderingComposer,
i1.$$MetadataEntityTableAnnotationComposer,
$$MetadataEntityTableCreateCompanionBuilder,
$$MetadataEntityTableUpdateCompanionBuilder,
i1.$SettingsEntityTable,
i1.SettingsEntityData,
i1.$$SettingsEntityTableFilterComposer,
i1.$$SettingsEntityTableOrderingComposer,
i1.$$SettingsEntityTableAnnotationComposer,
$$SettingsEntityTableCreateCompanionBuilder,
$$SettingsEntityTableUpdateCompanionBuilder,
(
i1.MetadataEntityData,
i1.SettingsEntityData,
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$MetadataEntityTable,
i1.MetadataEntityData
i1.$SettingsEntityTable,
i1.SettingsEntityData
>,
),
i1.MetadataEntityData,
i1.SettingsEntityData,
i0.PrefetchHooks Function()
>;
class $MetadataEntityTable extends i2.MetadataEntity
with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> {
class $SettingsEntityTable extends i2.SettingsEntity
with i0.TableInfo<$SettingsEntityTable, i1.SettingsEntityData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$MetadataEntityTable(this.attachedDatabase, [this._alias]);
$SettingsEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
@override
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
@@ -219,10 +219,10 @@ class $MetadataEntityTable extends i2.MetadataEntity
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'metadata';
static const String $name = 'settings';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.MetadataEntityData> instance, {
i0.Insertable<i1.SettingsEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
@@ -255,9 +255,9 @@ class $MetadataEntityTable extends i2.MetadataEntity
@override
Set<i0.GeneratedColumn> get $primaryKey => {key};
@override
i1.MetadataEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
i1.SettingsEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.MetadataEntityData(
return i1.SettingsEntityData(
key: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}key'],
@@ -274,8 +274,8 @@ class $MetadataEntityTable extends i2.MetadataEntity
}
@override
$MetadataEntityTable createAlias(String alias) {
return $MetadataEntityTable(attachedDatabase, alias);
$SettingsEntityTable createAlias(String alias) {
return $SettingsEntityTable(attachedDatabase, alias);
}
@override
@@ -284,12 +284,12 @@ class $MetadataEntityTable extends i2.MetadataEntity
bool get isStrict => true;
}
class MetadataEntityData extends i0.DataClass
implements i0.Insertable<i1.MetadataEntityData> {
class SettingsEntityData extends i0.DataClass
implements i0.Insertable<i1.SettingsEntityData> {
final String key;
final String value;
final DateTime updatedAt;
const MetadataEntityData({
const SettingsEntityData({
required this.key,
required this.value,
required this.updatedAt,
@@ -303,12 +303,12 @@ class MetadataEntityData extends i0.DataClass
return map;
}
factory MetadataEntityData.fromJson(
factory SettingsEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return MetadataEntityData(
return SettingsEntityData(
key: serializer.fromJson<String>(json['key']),
value: serializer.fromJson<String>(json['value']),
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
@@ -324,17 +324,17 @@ class MetadataEntityData extends i0.DataClass
};
}
i1.MetadataEntityData copyWith({
i1.SettingsEntityData copyWith({
String? key,
String? value,
DateTime? updatedAt,
}) => i1.MetadataEntityData(
}) => i1.SettingsEntityData(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) {
return MetadataEntityData(
SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) {
return SettingsEntityData(
key: data.key.present ? data.key.value : this.key,
value: data.value.present ? data.value.value : this.value,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
@@ -343,7 +343,7 @@ class MetadataEntityData extends i0.DataClass
@override
String toString() {
return (StringBuffer('MetadataEntityData(')
return (StringBuffer('SettingsEntityData(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
@@ -356,29 +356,29 @@ class MetadataEntityData extends i0.DataClass
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.MetadataEntityData &&
(other is i1.SettingsEntityData &&
other.key == this.key &&
other.value == this.value &&
other.updatedAt == this.updatedAt);
}
class MetadataEntityCompanion
extends i0.UpdateCompanion<i1.MetadataEntityData> {
class SettingsEntityCompanion
extends i0.UpdateCompanion<i1.SettingsEntityData> {
final i0.Value<String> key;
final i0.Value<String> value;
final i0.Value<DateTime> updatedAt;
const MetadataEntityCompanion({
const SettingsEntityCompanion({
this.key = const i0.Value.absent(),
this.value = const i0.Value.absent(),
this.updatedAt = const i0.Value.absent(),
});
MetadataEntityCompanion.insert({
SettingsEntityCompanion.insert({
required String key,
required String value,
this.updatedAt = const i0.Value.absent(),
}) : key = i0.Value(key),
value = i0.Value(value);
static i0.Insertable<i1.MetadataEntityData> custom({
static i0.Insertable<i1.SettingsEntityData> custom({
i0.Expression<String>? key,
i0.Expression<String>? value,
i0.Expression<DateTime>? updatedAt,
@@ -390,12 +390,12 @@ class MetadataEntityCompanion
});
}
i1.MetadataEntityCompanion copyWith({
i1.SettingsEntityCompanion copyWith({
i0.Value<String>? key,
i0.Value<String>? value,
i0.Value<DateTime>? updatedAt,
}) {
return i1.MetadataEntityCompanion(
return i1.SettingsEntityCompanion(
key: key ?? this.key,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
@@ -419,7 +419,7 @@ class MetadataEntityCompanion
@override
String toString() {
return (StringBuffer('MetadataEntityCompanion(')
return (StringBuffer('SettingsEntityCompanion(')
..write('key: $key, ')
..write('value: $value, ')
..write('updatedAt: $updatedAt')
@@ -13,7 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
@@ -55,7 +55,7 @@ import 'package:logging/logging.dart';
StoreEntity,
TrashedLocalAssetEntity,
AssetEditEntity,
MetadataEntity,
SettingsEntity,
],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
)
@@ -98,7 +98,7 @@ class Drift extends $Drift {
}
@override
int get schemaVersion => 26;
int get schemaVersion => 27;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -276,6 +276,9 @@ class Drift extends $Drift {
from25To26: (m, v26) async {
await m.addColumn(v26.remoteAssetEntity, v26.remoteAssetEntity.uploadedAt);
},
from26To27: (m, v27) async {
await customStatement('ALTER TABLE metadata RENAME TO settings');
},
),
);
@@ -43,7 +43,7 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
as i20;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i21;
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
as i22;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i23;
@@ -91,7 +91,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$TrashedLocalAssetEntityTable(this);
late final i21.$AssetEditEntityTable assetEditEntity = i21
.$AssetEditEntityTable(this);
late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable(
late final i22.$SettingsEntityTable settingsEntity = i22.$SettingsEntityTable(
this,
);
i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer(
@@ -132,7 +132,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
metadataEntity,
settingsEntity,
i10.idxPartnerSharedWithId,
i11.idxLatLng,
i11.idxRemoteExifCity,
@@ -395,6 +395,6 @@ class $DriftManager {
);
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
i22.$$MetadataEntityTableTableManager get metadataEntity =>
i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity);
i22.$$SettingsEntityTableTableManager get settingsEntity =>
i22.$$SettingsEntityTableTableManager(_db, _db.settingsEntity);
}
@@ -13539,6 +13539,550 @@ i1.GeneratedColumn<String> _column_212(String aliasedName) =>
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
final class Schema27 extends i0.VersionedSchema {
Schema27({required super.database}) : super(version: 27);
@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,
settings,
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 settings = Shape49(
source: i0.VersionedTable(
entityName: 'settings',
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)',
);
}
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -13565,6 +14109,7 @@ i0.MigrationStepWithVersion migrationSteps({
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,
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -13693,6 +14238,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from25To26(migrator, schema);
return 26;
case 26:
final schema = Schema27(database: database);
final migrator = i1.Migrator(database, schema);
await from26To27(migrator, schema);
return 27;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -13725,6 +14275,7 @@ i1.OnUpgrade stepByStep({
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,
required Future<void> Function(i1.Migrator m, Schema27 schema) from26To27,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -13752,5 +14303,6 @@ i1.OnUpgrade stepByStep({
from23To24: from23To24,
from24To25: from24To25,
from25To26: from25To26,
from26To27: from26To27,
),
);
@@ -1,177 +0,0 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class MetadataRepository extends DriftDatabaseRepository {
final Drift _db;
final Map<MetadataKey, Object> _cache = {};
MetadataRepository._(this._db) : super(_db);
static MetadataRepository? _instance;
static MetadataRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('MetadataRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
SystemConfig _systemConfig = const .new();
SystemConfig get systemConfig => _systemConfig;
static Future<MetadataRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = MetadataRepository._(db);
await instance._hydrate();
_instance = instance;
}
return _instance!;
}
static Future<void> refresh() async {
instance._cache.clear();
instance._appConfig = const .new();
instance._systemConfig = const .new();
await instance._hydrate();
}
Future<void> _hydrate() async => _hydrateCache(await _db.select(_db.metadataEntity).get());
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (_read(key) == value) {
return;
}
await _db
.into(_db.metadataEntity)
.insertOnConflictUpdate(
MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())),
);
_updateCache(key, value);
}
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
_updateCache(key, key.defaultValue);
}
Stream<AppConfig> watchAppConfig() => _watchDomain(.appConfig).distinct();
Stream<SystemConfig> watchSystemConfig() => _watchDomain(.systemConfig).distinct();
Stream<T> _watchDomain<T extends Object>(MetadataDomain<T> domain) {
final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%'));
return query.watch().map((rows) {
_hydrateCache(rows);
return domain.config(this);
});
}
void _hydrateCache(List<MetadataEntityData> rows) {
final keyMap = MetadataKey.asKeyMap();
for (final row in rows) {
final key = keyMap[row.key];
if (key == null) {
continue;
}
_updateCache(key, key.decode(row.value));
}
}
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
if (_cache[key] == value) {
return;
}
_cache[key] = value;
key.domain.rebuild(this);
}
}
extension<T extends Object> on MetadataDomain<T> {
T config(MetadataRepository repo) => switch (this) {
.appConfig => repo._appConfig as T,
.systemConfig => repo._systemConfig as T,
};
void rebuild(MetadataRepository repo) {
switch (this) {
case .appConfig:
repo._appConfig = .new(
theme: .new(
mode: repo._read(.themeMode),
primaryColor: repo._read(.themePrimaryColor),
dynamicTheme: repo._read(.themeDynamic),
colorfulInterface: repo._read(.themeColorfulInterface),
),
cleanup: .new(
keepFavorites: repo._read(.cleanupKeepFavorites),
keepMediaType: repo._read(.cleanupKeepMediaType),
keepAlbumIds: repo._read(.cleanupKeepAlbumIds),
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),
),
timeline: .new(
tilesPerRow: repo._read(.timelineTilesPerRow),
groupAssetsBy: repo._read(.timelineGroupAssetsBy),
storageIndicator: repo._read(.timelineStorageIndicator),
),
image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)),
viewer: .new(
loopVideo: repo._read(.viewerLoopVideo),
loadOriginalVideo: repo._read(.viewerLoadOriginalVideo),
autoPlayVideo: repo._read(.viewerAutoPlayVideo),
tapToNavigate: repo._read(.viewerTapToNavigate),
),
slideshow: .new(
transition: repo._read(.slideshowTransition),
repeat: repo._read(.slideshowRepeat),
duration: repo._read(.slideshowDuration),
look: repo._read(.slideshowLook),
direction: repo._read(.slideshowDirection),
),
album: .new(
sortMode: repo._read(.albumSortMode),
isReverse: repo._read(.albumIsReverse),
isGrid: repo._read(.albumIsGrid),
),
backup: .new(
enabled: repo._read(.backupEnabled),
useCellularForVideos: repo._read(.backupUseCellularForVideos),
useCellularForPhotos: repo._read(.backupUseCellularForPhotos),
requireCharging: repo._read(.backupRequireCharging),
triggerDelay: repo._read(.backupTriggerDelay),
syncAlbums: repo._read(.backupSyncAlbums),
),
);
case .systemConfig:
repo._systemConfig = .new(
logLevel: repo._read(.logLevel),
network: .new(
autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching),
preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty,
localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty,
externalEndpointList: repo._read(.networkExternalEndpointList),
customHeaders: repo._read(.networkCustomHeaders),
),
);
}
}
}
@@ -0,0 +1,84 @@
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
class SettingsRepository extends DriftDatabaseRepository {
final Drift _db;
SettingsRepository._(this._db) : super(_db);
static SettingsRepository? _instance;
static SettingsRepository get instance {
final instance = _instance;
if (instance == null) {
throw StateError('SettingsRepository not initialized. Call ensureInitialized() first');
}
return instance;
}
AppConfig _appConfig = const .new();
AppConfig get appConfig => _appConfig;
static Future<SettingsRepository> ensureInitialized(Drift db) async {
if (_instance == null) {
final instance = SettingsRepository._(db);
await instance.refresh();
_instance = instance;
}
return _instance!;
}
Future<void> refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get());
Future<void> clear(Iterable<SettingsKey> keys) async {
if (keys.isEmpty) {
return;
}
final names = keys.map((key) => key.name).toList();
await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go();
for (final key in keys) {
_appConfig = _appConfig.write(key, defaultConfig.read(key));
}
}
Future<void> write<T extends Object, U extends T>(SettingsKey<T> key, U value) async {
if (value == _appConfig.read(key)) {
return;
}
if (value == defaultConfig.read(key)) {
return clear([key]);
}
await _db
.into(_db.settingsEntity)
.insertOnConflictUpdate(
SettingsEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
);
_appConfig = _appConfig.write(key, value);
}
Stream<AppConfig> watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) {
_applyOverrides(rows);
return _appConfig;
});
void _applyOverrides(List<SettingsEntityData> rows) {
_appConfig = AppConfig.fromEntries(
rows.fold({}, (overrides, row) {
final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key);
if (metadataKey == null) {
return overrides;
}
return {...overrides, metadataKey: metadataKey.decode(row.value)};
}),
);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
@@ -8,11 +8,11 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/services/sync_linked_album.service.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
_searchController = TextEditingController();
_searchFocusNode = FocusNode();
_enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
_enableSyncUploadAlbum.value = ref.read(appConfigProvider).backup.syncAlbums;
ref.read(backupAlbumProvider.notifier).getAll();
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return;
}
final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
final enableSyncUploadAlbum = ref.read(appConfigProvider).backup.syncAlbums;
final selectedAlbums = ref
.read(backupAlbumProvider)
.where((a) => a.backupSelection == BackupSelection.selected)
@@ -103,7 +103,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return;
}
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled;
await ref.read(driftBackupProvider.notifier).getBackupStatus(user.id);
final currentTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
final totalChanged = currentTotalAssetCount != _initialTotalAssetCount;
@@ -4,10 +4,10 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart';
import 'package:logging/logging.dart';
@@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false;
final previousBackup = ref.read(metadataProvider).appConfig.backup;
final previousBackup = ref.read(appConfigProvider).backup;
final previousCellularForVideos = previousBackup.useCellularForVideos;
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
return PopScope(
@@ -27,7 +27,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
// There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped
final currentBackup = ref.read(metadataProvider).appConfig.backup;
final currentBackup = ref.read(appConfigProvider).backup;
final currentCellularForVideos = currentBackup.useCellularForVideos;
final currentCellularForPhotos = currentBackup.useCellularForPhotos;
@@ -45,7 +45,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
}
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
final isBackupEnabled = MetadataRepository.instance.appConfig.backup.enabled;
final isBackupEnabled = SettingsRepository.instance.appConfig.backup.enabled;
if (!isBackupEnabled) {
return;
}
@@ -3,10 +3,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
class SettingsHeader {
String key = "";
@@ -22,7 +21,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
final headers = useState<List<SettingsHeader>>([]);
final setInitialHeaders = useState(false);
final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
final storedHeaders = ref.read(appConfigProvider).network.customHeaders;
if (!setInitialHeaders.value) {
storedHeaders.forEach((k, v) {
final header = SettingsHeader();
@@ -94,7 +93,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
headersMap[key] = value;
}
await ref.read(metadataProvider).write(MetadataKey.networkCustomHeaders, headersMap);
await ref.read(settingsProvider).write(.networkCustomHeaders, headersMap);
await ref.read(apiServiceProvider).updateHeaders();
}
}
@@ -7,12 +7,12 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/locales.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
@@ -36,7 +36,7 @@ class BootstrapErrorWidget extends StatelessWidget {
@override
Widget build(BuildContext _) {
final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset;
final immichTheme = defaultConfig.theme.primaryColor.themeOfPreset;
return EasyLocalization(
supportedLocales: locales.values.toList(),
@@ -341,7 +341,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
await backgroundManager.hashAssets();
}
if (MetadataRepository.instance.appConfig.backup.syncAlbums) {
if (SettingsRepository.instance.appConfig.backup.syncAlbums) {
await backgroundManager.syncLinkedAlbum();
}
} catch (e) {
@@ -370,7 +370,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
}
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
final isEnableBackup = MetadataRepository.instance.appConfig.backup.enabled;
final isEnableBackup = SettingsRepository.instance.appConfig.backup.enabled;
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
@@ -17,7 +17,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.wid
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@@ -9,6 +9,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
/// This delete action has the following behavior:
/// - Prompt to delete the asset locally
@@ -39,6 +40,8 @@ class DeleteLocalActionButton extends ConsumerWidget {
return;
}
ref.invalidate(localAlbumProvider);
final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()});
if (context.mounted) {
@@ -14,7 +14,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class _SharePreparingDialog extends StatelessWidget {
const _SharePreparingDialog();
final ValueNotifier<double?> progress;
const _SharePreparingDialog({required this.progress});
@override
Widget build(BuildContext context) {
@@ -22,8 +24,24 @@ class _SharePreparingDialog extends StatelessWidget {
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()),
Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()),
SizedBox(
width: 240,
child: ValueListenableBuilder<double?>(
valueListenable: progress,
builder: (context, value, _) {
final percent = value == null ? null : (value * 100).clamp(0, 100);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(value: value, minHeight: 8.0),
if (percent != null)
Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')),
],
);
},
),
),
],
),
);
@@ -43,32 +61,39 @@ class ShareActionButton extends ConsumerWidget {
}
final cancelCompleter = Completer<void>();
const preparingDialog = _SharePreparingDialog();
final progress = ValueNotifier<double?>(null);
final preparingDialog = _SharePreparingDialog(progress: progress);
await showDialog(
context: context,
builder: (BuildContext buildContext) {
ref.read(actionProvider.notifier).shareAssets(source, context, cancelCompleter: cancelCompleter).then((
ActionResult result,
) {
if (cancelCompleter.isCompleted || !context.mounted) {
return;
}
ref
.read(actionProvider.notifier)
.shareAssets(
source,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: (value) => progress.value = value,
)
.then((ActionResult result) {
if (cancelCompleter.isCompleted || !context.mounted) {
return;
}
ref.read(multiSelectProvider.notifier).reset();
ref.read(multiSelectProvider.notifier).reset();
if (!result.success) {
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
if (!result.success) {
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
buildContext.pop();
});
buildContext.pop();
});
// show a loading spinner with a "Preparing" message
// Show download progress with a "Preparing" message
return preparingDialog;
},
barrierDismissible: false,
@@ -77,6 +102,7 @@ class ShareActionButton extends ConsumerWidget {
if (!cancelCompleter.isCompleted) {
cancelCompleter.complete();
}
progress.dispose();
});
}
@@ -15,12 +15,11 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/presentation/widgets/album/album_tile.dart';
import 'package:immich_mobile/presentation/widgets/album/new_album_name_modal.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
@@ -58,7 +57,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final albumConfig = ref.read(metadataProvider).appConfig.album;
final albumConfig = ref.read(appConfigProvider).album;
setState(() {
sort = AlbumSort(mode: albumConfig.sortMode, isReverse: albumConfig.isReverse);
@@ -94,7 +93,7 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
setState(() {
isGrid = !isGrid;
});
ref.read(metadataProvider).write(MetadataKey.albumIsGrid, isGrid);
ref.read(settingsProvider).write(.albumIsGrid, isGrid);
}
void changeFilter(QuickFilterMode mode) {
@@ -110,9 +109,9 @@ class _AlbumSelectorState extends ConsumerState<AlbumSelector> {
this.sort = sort;
});
final metadata = ref.read(metadataProvider);
await metadata.write(MetadataKey.albumSortMode, sort.mode);
await metadata.write(MetadataKey.albumIsReverse, sort.isReverse);
final metadata = ref.read(settingsProvider);
await metadata.write(.albumSortMode, sort.mode);
await metadata.write(.albumIsReverse, sort.isReverse);
await sortAlbums();
}
@@ -19,7 +19,7 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@@ -241,7 +241,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
return;
}
final tapToNavigate = ref.read(metadataProvider).appConfig.viewer.tapToNavigate;
final tapToNavigate = ref.read(appConfigProvider).viewer.tapToNavigate;
if (!tapToNavigate) {
_viewer.toggleControls();
return;
@@ -12,7 +12,7 @@ import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.pro
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:native_video_player/native_video_player.dart';
@@ -128,7 +128,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
final remoteId = (videoAsset as RemoteAsset).id;
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final isOriginalVideo = ref.read(metadataProvider).appConfig.viewer.loadOriginalVideo;
final isOriginalVideo = ref.read(appConfigProvider).viewer.loadOriginalVideo;
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = videoAsset.livePhotoVideoId != null
? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl'
@@ -161,7 +161,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
return;
}
final autoPlayVideo = ref.read(metadataProvider).appConfig.viewer.autoPlayVideo;
final autoPlayVideo = ref.read(appConfigProvider).viewer.autoPlayVideo;
if (autoPlayVideo || widget.asset.isMotionPhoto) {
await _notifier.play();
}
@@ -212,7 +212,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
}
await _notifier.load(source);
final loopVideo = ref.read(metadataProvider).appConfig.viewer.loopVideo;
final loopVideo = ref.read(appConfigProvider).viewer.loopVideo;
await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo);
await _notifier.setVolume(1);
}
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
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/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
@@ -34,7 +33,7 @@ class ViewerKebabMenu extends ConsumerWidget {
final isInLockedView = ref.watch(inLockedViewProvider);
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(.advancedTroubleshooting);
final actionContext = ActionButtonContext(
asset: asset,
@@ -1,10 +1,9 @@
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
class BackupToggleButton extends ConsumerStatefulWidget {
final VoidCallback onStart;
@@ -31,7 +30,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
end: 1,
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
_isEnabled = ref.read(metadataProvider).appConfig.backup.enabled;
_isEnabled = ref.read(appConfigProvider).backup.enabled;
}
@override
@@ -41,7 +40,7 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
}
Future<void> _onToggle(bool value) async {
await ref.read(metadataProvider).write(MetadataKey.backupEnabled, value);
await ref.read(settingsProvider).write(.backupEnabled, value);
setState(() {
_isEnabled = value;
@@ -4,7 +4,7 @@ import 'package:async/async.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
@@ -189,5 +189,5 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
bool _shouldUseLocalAsset(BaseAsset asset) =>
asset.hasLocal &&
(!asset.hasRemote || !MetadataRepository.instance.appConfig.image.preferRemote) &&
(!asset.hasRemote || !SettingsRepository.instance.appConfig.image.preferRemote) &&
!asset.isEdited;
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@@ -104,7 +104,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
return;
}
final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal;
final loadOriginal = SettingsRepository.instance.appConfig.image.loadOriginal;
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest(
localId: key.id,
@@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@@ -122,7 +122,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
edited: key.edited,
),
);
final loadOriginal = assetType == AssetType.image && MetadataRepository.instance.appConfig.image.loadOriginal;
final loadOriginal = assetType == AssetType.image && SettingsRepository.instance.appConfig.image.loadOriginal;
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
@@ -296,16 +296,12 @@ class _ThumbnailRenderBox extends RenderBox {
bool isRepaintBoundary = true;
_ThumbnailRenderBox({
required ui.Image? image,
required ui.Image? previousImage,
required double fadeValue,
required BoxFit fit,
required Gradient placeholderGradient,
}) : _image = image,
_previousImage = previousImage,
_fadeValue = fadeValue,
_fit = fit,
_placeholderGradient = placeholderGradient;
required this._image,
required this._previousImage,
required this._fadeValue,
required this._fit,
required this._placeholderGradient,
});
@override
void paint(PaintingContext context, Offset offset) {
@@ -9,7 +9,7 @@ import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class ThumbnailTile extends ConsumerStatefulWidget {
@@ -1,11 +1,10 @@
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/infrastructure/map.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@@ -81,25 +80,25 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly);
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(onlyFavorites: isFavoriteOnly);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchIncludeArchived(bool isIncludeArchived) {
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived);
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchWithPartners(bool isWithPartners) {
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners);
ref.read(settingsProvider).write(.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void setRelativeTime(int relativeDays) {
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays);
ref.read(settingsProvider).write(.mapRelativeDate, relativeDays);
state = state.copyWith(relativeDays: relativeDays);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
@@ -62,14 +62,11 @@ class RenderFixedRow extends RenderBox
RenderBoxContainerDefaultsMixin<RenderBox, _RowParentData> {
RenderFixedRow({
List<RenderBox>? children,
required double height,
required List<double> widths,
required double spacing,
required TextDirection textDirection,
}) : _height = height,
_widths = widths,
_spacing = spacing,
_textDirection = textDirection {
required this._height,
required this._widths,
required this._spacing,
required this._textDirection,
}) {
addAll(children);
}
@@ -578,9 +578,7 @@ class _SlideFadeTransition extends StatelessWidget {
final Animation<double> _animation;
final Widget _child;
const _SlideFadeTransition({required Animation<double> animation, required Widget child})
: _animation = animation,
_child = child;
const _SlideFadeTransition({required this._animation, required this._child});
@override
Widget build(BuildContext context) {
@@ -5,7 +5,7 @@ import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class TimelineArgs {
@@ -10,7 +10,6 @@ import 'package:flutter/rendering.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@@ -22,7 +21,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -397,7 +396,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
final grid = CustomScrollView(
primary: true,
physics: _scrollPhysics,
cacheExtent: maxHeight * 2,
scrollCacheExtent: .pixels(maxHeight * 2),
slivers: [
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
if (widget.topSliverWidget != null) widget.topSliverWidget!,
@@ -459,7 +458,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_restoreAssetIndex = targetAssetIndex;
});
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow);
ref.read(settingsProvider).write(.timelineTilesPerRow, _perRow);
}
};
},
@@ -503,7 +502,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
final List<Segment> _segments;
const _SliverSegmentedList({required List<Segment> segments, required super.delegate}) : _segments = segments;
const _SliverSegmentedList({required this._segments, required super.delegate});
@override
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
@@ -527,8 +526,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
markNeedsLayout();
}
_RenderSliverTimelineBoxAdaptor({required super.childManager, required List<Segment> segments})
: _segments = segments;
_RenderSliverTimelineBoxAdaptor({required super.childManager, required this._segments});
int getMinChildIndexForScrollOffset(double offset) =>
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
@@ -9,8 +9,8 @@ import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
@@ -107,7 +107,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
await Future.delayed(const Duration(milliseconds: 500));
final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
final isAlbumLinkedSyncEnable = _ref.read(appConfigProvider).backup.syncAlbums;
try {
bool syncSuccess = false;
@@ -137,7 +137,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
}
Future<void> _resumeBackup() async {
final isEnableBackup = _ref.read(metadataProvider).appConfig.backup.enabled;
final isEnableBackup = _ref.read(appConfigProvider).backup.enabled;
if (isEnableBackup) {
final currentUser = Store.tryGet(StoreKey.currentUser);
+6 -7
View File
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
@@ -11,7 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
@@ -130,7 +129,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
await _apiService.updateHeaders();
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders;
final headerMap = _ref.read(appConfigProvider).network.customHeaders;
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
@@ -179,19 +178,19 @@ class AuthNotifier extends StateNotifier<AuthState> {
}
Future<void> saveWifiName(String wifiName) async {
await _ref.read(metadataProvider).write(MetadataKey.networkPreferredWifiName, wifiName);
await _ref.read(settingsProvider).write(.networkPreferredWifiName, wifiName);
}
Future<void> saveLocalEndpoint(String url) async {
await _ref.read(metadataProvider).write(MetadataKey.networkLocalEndpoint, url);
await _ref.read(settingsProvider).write(.networkLocalEndpoint, url);
}
String? getSavedWifiName() {
return _ref.read(metadataProvider).systemConfig.network.preferredWifiName;
return _ref.read(appConfigProvider).network.preferredWifiName;
}
String? getSavedLocalEndpoint() {
return _ref.read(metadataProvider).systemConfig.network.localEndpoint;
return _ref.read(appConfigProvider).network.localEndpoint;
}
/// Returns the current server endpoint (with /api) URL from the store
+12 -12
View File
@@ -1,8 +1,8 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/cleanup.service.dart';
@@ -54,21 +54,21 @@ final cleanupProvider = StateNotifierProvider<CleanupNotifier, CleanupState>((re
return CleanupNotifier(
ref.watch(cleanupServiceProvider),
ref.watch(currentUserProvider)?.id,
ref.watch(metadataProvider),
ref.watch(settingsProvider),
);
});
class CleanupNotifier extends StateNotifier<CleanupState> {
final CleanupService _cleanupService;
final String? _userId;
final MetadataRepository _metadataRepository;
final SettingsRepository _settingsRepository;
CleanupNotifier(this._cleanupService, this._userId, this._metadataRepository) : super(const CleanupState()) {
CleanupNotifier(this._cleanupService, this._userId, this._settingsRepository) : super(const CleanupState()) {
_loadPersistedSettings();
}
void _loadPersistedSettings() {
final cleanup = _metadataRepository.appConfig.cleanup;
final cleanup = _settingsRepository.appConfig.cleanup;
final keepFavorites = cleanup.keepFavorites;
final keepMediaType = cleanup.keepMediaType;
final keepAlbumIds = cleanup.keepAlbumIds.toSet();
@@ -87,18 +87,18 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
state = state.copyWith(selectedDate: date, assetsToDelete: []);
if (date != null) {
final daysAgo = DateTime.now().difference(date).inDays;
_metadataRepository.write(.cleanupCutoffDaysAgo, daysAgo);
_settingsRepository.write(.cleanupCutoffDaysAgo, daysAgo);
}
}
void setKeepMediaType(AssetKeepType keepMediaType) {
state = state.copyWith(keepMediaType: keepMediaType, assetsToDelete: []);
_metadataRepository.write(.cleanupKeepMediaType, keepMediaType);
_settingsRepository.write(.cleanupKeepMediaType, keepMediaType);
}
void setKeepFavorites(bool keepFavorites) {
state = state.copyWith(keepFavorites: keepFavorites, assetsToDelete: []);
_metadataRepository.write(.cleanupKeepFavorites, keepFavorites);
_settingsRepository.write(.cleanupKeepFavorites, keepFavorites);
}
void toggleKeepAlbum(String albumId) {
@@ -118,7 +118,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
}
void _persistExcludedAlbumIds(Set<String> albumIds) {
_metadataRepository.write(.cleanupKeepAlbumIds, albumIds.toList());
_settingsRepository.write(.cleanupKeepAlbumIds, albumIds.toList());
}
void cleanupStaleAlbumIds(Set<String> existingAlbumIds) {
@@ -131,7 +131,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
}
void applyDefaultAlbumSelections(List<(String id, String name)> albums) {
final isInitialized = _metadataRepository.appConfig.cleanup.defaultsInitialized;
final isInitialized = _settingsRepository.appConfig.cleanup.defaultsInitialized;
if (isInitialized) {
return;
}
@@ -144,7 +144,7 @@ class CleanupNotifier extends StateNotifier<CleanupState> {
_persistExcludedAlbumIds(keepAlbumIds);
}
_metadataRepository.write(.cleanupDefaultsInitialized, true);
_settingsRepository.write(.cleanupDefaultsInitialized, true);
}
Future<void> scanAssets() async {
@@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.da
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
import 'package:immich_mobile/providers/infrastructure/tag.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
@@ -21,6 +22,7 @@ import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/services/action.service.dart';
import 'package:immich_mobile/services/download.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/utils/semver.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -463,11 +465,17 @@ class ActionNotifier extends Notifier<void> {
ActionSource source,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) async {
final ids = _getAssets(source).toList(growable: false);
try {
await _service.shareAssets(ids, context, cancelCompleter: cancelCompleter);
await _service.shareAssets(
ids,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: onAssetDownloadProgress,
);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to share assets', error, stack);
@@ -536,14 +544,22 @@ class ActionNotifier extends Notifier<void> {
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits');
}
final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) {
final eventAsset = SyncAssetV1.fromJson(data["asset"]);
return eventAsset?.id == ids.first;
}, const Duration(seconds: 10));
Future<void> editReady;
if (ref.read(serverInfoProvider).serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) {
editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV2", (dynamic data) {
final eventAsset = SyncAssetV2.fromJson(data["asset"]);
return eventAsset?.id == ids.first;
}, const Duration(seconds: 10));
} else {
editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) {
final eventAsset = SyncAssetV1.fromJson(data["asset"]);
return eventAsset?.id == ids.first;
}, const Duration(seconds: 10));
}
try {
await _service.applyEdits(ids.first, edits);
await completer;
await editReady;
return const ActionResult(count: 1, success: true);
} catch (error, stack) {
_logger.severe('Failed to apply edits to assets', error, stack);
@@ -1,20 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_config.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance);
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(metadataProvider);
final subscription = repo.watchAppConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.appConfig;
});
final systemConfigProvider = Provider.autoDispose<SystemConfig>((ref) {
final repo = ref.watch(metadataProvider);
final subscription = repo.watchSystemConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.systemConfig;
});
@@ -0,0 +1,12 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
final settingsProvider = Provider.autoDispose<SettingsRepository>((_) => SettingsRepository.instance);
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(settingsProvider);
final subscription = repo.watchConfig().listen((event) => ref.state = event);
ref.onDispose(subscription.cancel);
return repo.appConfig;
});
@@ -3,7 +3,7 @@ import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
final timelineRepositoryProvider = Provider<DriftTimelineRepository>(
@@ -29,7 +29,7 @@ final timelineServiceProvider = Provider<TimelineService>(
final timelineFactoryProvider = Provider<TimelineFactory>(
(ref) => TimelineFactory(
timelineRepository: ref.watch(timelineRepositoryProvider),
metadataRepository: ref.watch(metadataProvider),
settingsRepository: ref.watch(settingsProvider),
),
);
@@ -1,8 +1,7 @@
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/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
final mapStateNotifierProvider = NotifierProvider<MapStateNotifier, MapState>(MapStateNotifier.new);
@@ -27,12 +26,12 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchTheme(ThemeMode mode) {
ref.read(metadataProvider).write(MetadataKey.mapThemeMode, mode);
ref.read(settingsProvider).write(.mapThemeMode, mode);
state = state.copyWith(themeMode: mode);
}
void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly);
ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(showFavoriteOnly: isFavoriteOnly, shouldRefetchMarkers: true);
}
@@ -41,17 +40,17 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchIncludeArchived(bool isIncludeArchived) {
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived);
ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived, shouldRefetchMarkers: true);
}
void switchWithPartners(bool isWithPartners) {
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners);
ref.read(settingsProvider).write(.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners, shouldRefetchMarkers: true);
}
void setRelativeTime(int relativeTime) {
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeTime);
ref.read(settingsProvider).write(.mapRelativeDate, relativeTime);
state = state.copyWith(relativeTime: relativeTime, shouldRefetchMarkers: true);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/theme/color_scheme.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/theme/theme_data.dart';
+3 -3
View File
@@ -7,7 +7,7 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
import 'package:immich_mobile/models/server_info/server_version.model.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/debug_print.dart';
@@ -193,7 +193,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums;
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
@@ -214,7 +214,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
return;
}
final isSyncAlbumEnabled = _ref.read(metadataProvider).appConfig.backup.syncAlbums;
final isSyncAlbumEnabled = _ref.read(appConfigProvider).backup.syncAlbums;
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
@@ -1,31 +1,29 @@
import 'dart:async';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:share_plus/share_plus.dart';
final assetMediaRepositoryProvider = Provider(
(ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider), ref.watch(nativeSyncApiProvider)),
);
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(nativeSyncApiProvider)));
class AssetMediaRepository {
final AssetApiRepository _assetApiRepository;
final NativeSyncApi _nativeSyncApi;
static final Logger _log = Logger("AssetMediaRepository");
const AssetMediaRepository(this._assetApiRepository, this._nativeSyncApi);
const AssetMediaRepository(this._nativeSyncApi);
Future<bool> _androidSupportsTrash() async {
if (Platform.isAndroid) {
@@ -107,10 +105,29 @@ class AssetMediaRepository {
);
}
// TODO: make this more efficient
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) async {
Future<int> shareAssets(
List<BaseAsset> assets,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) async {
final downloadedXFiles = <XFile>[];
final tempFiles = <File>[];
final totalAssets = assets.length;
var processedAssets = 0;
void updateProgress([double currentAssetProgress = 0.0]) {
if (totalAssets <= 0) {
onAssetDownloadProgress?.call(1.0);
return;
}
final normalizedAssetProgress = currentAssetProgress.clamp(0.0, 1.0);
final overallProgress = ((processedAssets + normalizedAssetProgress) / totalAssets).clamp(0.0, 1.0);
onAssetDownloadProgress?.call(overallProgress);
}
updateProgress();
for (var asset in assets) {
if (cancelCompleter != null && cancelCompleter.isCompleted) {
@@ -127,6 +144,8 @@ class AssetMediaRepository {
if (localId != null && !asset.isEdited) {
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
downloadedXFiles.add(XFile(f!.path));
processedAssets++;
updateProgress();
if (CurrentPlatform.isIOS) {
tempFiles.add(f);
}
@@ -134,22 +153,50 @@ class AssetMediaRepository {
final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId;
if (remoteId == null) {
_log.warning("Asset has no remote ID for sharing: $asset");
processedAssets++;
updateProgress();
continue;
}
final tempDir = await getTemporaryDirectory();
final name = asset.name;
final tempFile = await File('${tempDir.path}/$name').create();
final res = await _assetApiRepository.downloadAsset(remoteId, edited: true);
final taskId = 'share-$remoteId-${DateTime.now().microsecondsSinceEpoch}';
final sanitizedFilename = asset.name.replaceAll(RegExp(r'[\\/]'), '_');
final task = DownloadTask(
taskId: taskId,
url: getOriginalUrlForRemoteId(remoteId, edited: asset.isEdited),
headers: ApiService.getRequestHeaders(),
filename: sanitizedFilename,
baseDirectory: BaseDirectory.temporary,
group: kShareDownloadGroup,
updates: Updates.statusAndProgress,
);
final statusUpdate = await FileDownloader().download(
task,
onProgress: (value) {
if (cancelCompleter != null && cancelCompleter.isCompleted) {
unawaited(FileDownloader().cancelTaskWithId(taskId));
return;
}
updateProgress(value);
},
);
if (res.statusCode != 200) {
_log.severe("Download for $name failed", res.toLoggerString());
continue;
if (cancelCompleter != null && cancelCompleter.isCompleted) {
await _cleanupTempFiles(tempFiles);
return 0;
}
await tempFile.writeAsBytes(res.bodyBytes);
downloadedXFiles.add(XFile(tempFile.path));
tempFiles.add(tempFile);
if (statusUpdate.status == TaskStatus.complete) {
final filePath = await task.filePath();
final file = File(filePath);
tempFiles.add(file);
downloadedXFiles.add(XFile(filePath));
processedAssets++;
updateProgress();
continue;
}
_log.severe("Download for ${asset.name} failed with status ${statusUpdate.status}", statusUpdate.exception);
processedAssets++;
updateProgress();
}
}
+9 -11
View File
@@ -1,40 +1,38 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/config/app_config.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)),
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)),
);
class AuthRepository {
final Drift _drift;
final MetadataRepository _metadata;
final AppConfig _config;
const AuthRepository(this._drift, this._metadata);
const AuthRepository(this._drift, this._config);
Future<void> clearLocalData() async {
await SyncStreamRepository(_drift).reset();
}
bool getEndpointSwitchingFeature() {
return _metadata.systemConfig.network.autoEndpointSwitching;
return _config.network.autoEndpointSwitching;
}
String? getPreferredWifiName() {
return _metadata.systemConfig.network.preferredWifiName;
return _config.network.preferredWifiName;
}
String? getLocalEndpoint() {
return _metadata.systemConfig.network.localEndpoint;
return _config.network.localEndpoint;
}
List<AuxilaryEndpoint> getExternalEndpointList() {
return _metadata.systemConfig.network.externalEndpointList
.map((url) => AuxilaryEndpoint(url: url, status: .valid))
.toList();
return _config.network.externalEndpointList.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList();
}
}
+12 -2
View File
@@ -269,8 +269,18 @@ class ActionService {
await _assetApiRepository.unStack(stackIds);
}
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) {
return _assetMediaRepository.shareAssets(assets, context, cancelCompleter: cancelCompleter);
Future<int> shareAssets(
List<BaseAsset> assets,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) {
return _assetMediaRepository.shareAssets(
assets,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: onAssetDownloadProgress,
);
}
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
+6 -6
View File
@@ -5,7 +5,7 @@ import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/utils/url_helper.dart';
@@ -13,7 +13,7 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class ApiService {
late ApiClient _apiClient;
final ApiClient _apiClient = ApiClient(basePath: '');
late UsersApi usersApi;
late AuthenticationApi authenticationApi;
@@ -54,7 +54,7 @@ class ApiService {
}
setEndpoint(String endpoint) {
_apiClient = ApiClient(basePath: endpoint);
_apiClient.basePath = endpoint;
_apiClient.client = NetworkRepository.client;
usersApi = UsersApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient);
@@ -177,9 +177,9 @@ class ApiService {
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
urls.add(serverEndpoint);
}
final network = MetadataRepository.instance.systemConfig.network;
final network = SettingsRepository.instance.appConfig.network;
final localEndpoint = network.localEndpoint;
if (localEndpoint != null) {
if (localEndpoint.isNotEmpty) {
urls.add(localEndpoint);
}
for (final url in network.externalEndpointList) {
@@ -191,7 +191,7 @@ class ApiService {
}
static Map<String, String> getRequestHeaders() {
return MetadataRepository.instance.systemConfig.network.customHeaders;
return SettingsRepository.instance.appConfig.network.customHeaders;
}
ApiClient get apiClient => _apiClient;
+10 -4
View File
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/settings_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
@@ -100,7 +100,7 @@ class AuthService {
_log.severe("Error clearing local data", error, stackTrace);
});
await MetadataRepository.instance.write(MetadataKey.backupEnabled, false);
await SettingsRepository.instance.write(SettingsKey.backupEnabled, false);
}
}
@@ -110,7 +110,7 @@ class AuthService {
/// - Authentication repository data
/// - Current user information
/// - Access token
/// - Asset ETag
/// - Server-specific endpoint configuration
///
/// All deletions are executed in parallel using [Future.wait].
Future<void> clearLocalData() async {
@@ -120,6 +120,12 @@ class AuthService {
_authRepository.clearLocalData(),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
SettingsRepository.instance.clear(const [
.networkAutoEndpointSwitching,
.networkPreferredWifiName,
.networkLocalEndpoint,
.networkExternalEndpointList,
]),
]);
}
@@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
@@ -359,7 +359,7 @@ class BackgroundUploadService {
}
bool _shouldRequireWiFi(LocalAsset asset) {
final backup = MetadataRepository.instance.appConfig.backup;
final backup = SettingsRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
return false;
}
@@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/network_capability_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
@@ -451,7 +451,7 @@ class ForegroundUploadService {
}
bool _shouldRequireWiFi(LocalAsset asset) {
final backup = MetadataRepository.instance.appConfig.backup;
final backup = SettingsRepository.instance.appConfig.backup;
if (asset.isVideo && backup.useCellularForVideos) {
return false;
}

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