mirror of
https://github.com/immich-app/immich.git
synced 2026-05-29 11:02:38 -04:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e35610d0a7 | |||
| bcff1d42b0 | |||
| 1bd367bd51 | |||
| 725f266b81 | |||
| d08e3de207 | |||
| 26714f6bfe | |||
| a5ce3fc927 | |||
| 3b23f71a3f | |||
| dec33cadd9 | |||
| 80c15a5e27 | |||
| 936c28a40b | |||
| 1a837a28ac | |||
| 8d5d12b108 | |||
| dd7a94135f | |||
| 1acc511b5c | |||
| 452e88267a | |||
| b941108cbd | |||
| e46f2843f7 | |||
| cf991e7b1b | |||
| 748a13104a | |||
| 2dd6b47714 | |||
| 8682be4774 | |||
| dc66892ca1 | |||
| 53a24783f5 | |||
| 0546bc900c | |||
| 7c25bcc0a7 | |||
| 7905853639 | |||
| 073dcc1fbe | |||
| ccdaa4223c | |||
| 5386b62dc4 | |||
| 9733fa4872 | |||
| 3b34c53092 | |||
| fd7ddfef54 | |||
| 0975b1599c | |||
| 78ac0ade01 |
@@ -8,6 +8,8 @@ log "Preparing Immich Web Frontend"
|
|||||||
log ""
|
log ""
|
||||||
run_cmd pnpm --filter @immich/sdk install
|
run_cmd pnpm --filter @immich/sdk install
|
||||||
run_cmd pnpm --filter @immich/sdk build
|
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
|
run_cmd pnpm --filter immich-web install
|
||||||
|
|
||||||
log "Starting Immich Web Frontend"
|
log "Starting Immich Web Frontend"
|
||||||
|
|||||||
@@ -230,8 +230,12 @@ jobs:
|
|||||||
- name: Generate platform APIs
|
- name: Generate platform APIs
|
||||||
run: mise //mobile:codegen:pigeon
|
run: mise //mobile:codegen:pigeon
|
||||||
|
|
||||||
|
- name: Resolve iOS Swift Packages
|
||||||
|
working-directory: ./mobile
|
||||||
|
run: flutter build ios --config-only --no-codesign
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
|
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# 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).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- 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.
|
# ℹ️ 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
|
# 📚 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
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
dev:
|
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:
|
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:
|
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:
|
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:
|
dev-docs:
|
||||||
npm --prefix docs run start
|
npm --prefix docs run start
|
||||||
|
|
||||||
.PHONY: e2e
|
.PHONY: e2e
|
||||||
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:
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
.PHONY: open-api
|
||||||
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:
|
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:
|
renovate:
|
||||||
@@ -52,16 +52,7 @@ renovate:
|
|||||||
MODULES = e2e server web cli sdk docs .github
|
MODULES = e2e server web cli sdk docs .github
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml build
|
@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
|
||||||
pnpm --filter immich-e2e run test
|
|
||||||
pnpm --filter immich-e2e run test:web
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
||||||
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
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ..:/usr/src/app
|
||||||
# - ../../ui:/usr/src/ui
|
# - ../../ui:/usr/src/ui
|
||||||
- pnpm_cache:/buildcache/pnpm_cache
|
- build_cache:/buildcache
|
||||||
- server_node_modules:/usr/src/app/server/node_modules
|
- server_node_modules:/usr/src/app/server/node_modules
|
||||||
- web_node_modules:/usr/src/app/web/node_modules
|
- web_node_modules:/usr/src/app/web/node_modules
|
||||||
- github_node_modules:/usr/src/app/.github/node_modules
|
- github_node_modules:/usr/src/app/.github/node_modules
|
||||||
@@ -45,11 +45,11 @@ services:
|
|||||||
target: dev
|
target: dev
|
||||||
command:
|
command:
|
||||||
- |
|
- |
|
||||||
pnpm install
|
mise install
|
||||||
touch /tmp/init-complete
|
touch /tmp/init-complete
|
||||||
exec tail -f /dev/null
|
exec tail -f /dev/null
|
||||||
volumes:
|
volumes:
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
- build_cache:/buildcache
|
||||||
restart: 'no'
|
restart: 'no'
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ['CMD', 'test', '-f', '/tmp/init-complete']
|
test: ['CMD', 'test', '-f', '/tmp/init-complete']
|
||||||
@@ -73,7 +73,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}/photos:/data
|
- ${UPLOAD_LOCATION}/photos:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm_store_server:/buildcache/pnpm-store
|
|
||||||
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -122,8 +121,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
- 24678:24678
|
- 24678:24678
|
||||||
volumes:
|
|
||||||
- pnpm_store_web:/buildcache/pnpm-store
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
immich-init:
|
immich-init:
|
||||||
@@ -203,9 +200,7 @@ volumes:
|
|||||||
model_cache:
|
model_cache:
|
||||||
prometheus_data:
|
prometheus_data:
|
||||||
grafana_data:
|
grafana_data:
|
||||||
pnpm_cache:
|
build_cache:
|
||||||
pnpm_store_server:
|
|
||||||
pnpm_store_web:
|
|
||||||
server_node_modules:
|
server_node_modules:
|
||||||
web_node_modules:
|
web_node_modules:
|
||||||
github_node_modules:
|
github_node_modules:
|
||||||
|
|||||||
@@ -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
|
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
|
## User
|
||||||
|
|
||||||
### How can I reset the admin password?
|
### 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.
|
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
|
## Mobile App
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ After making any changes in the `server/src/schema`, a database migration need t
|
|||||||
1. Run the command
|
1. Run the command
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run migrations:generate <migration-name>
|
mise //server:migrations generate <migration-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Check if the migration file makes sense.
|
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:
|
If you need to undo the most recently applied migration—for example, when developing or testing on schema changes—run:
|
||||||
|
|
||||||
```bash
|
```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.
|
This command rolls back the latest migration and brings the database schema back to its previous state.
|
||||||
|
|||||||
@@ -252,44 +252,33 @@ To connect the mobile app to your Dev Container:
|
|||||||
|
|
||||||
The Dev Container supports multiple ways to run tests:
|
The Dev Container supports multiple ways to run tests:
|
||||||
|
|
||||||
#### Using Mise Commands (Recommended)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests for specific components
|
# Server
|
||||||
mise run checklist # in `server/`, `web/`, `packages/cli`
|
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
|
### Additional Commands
|
||||||
|
|
||||||
```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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# API generation
|
# API generation
|
||||||
make open-api # Generate OpenAPI specs
|
mise //:open-api # Generate OpenAPI specs
|
||||||
make open-api-typescript # Generate TypeScript SDK
|
mise //:open-api-typescript # Generate TypeScript SDK
|
||||||
make open-api-dart # Generate Dart SDK
|
mise //:open-api-dart # Generate Dart SDK
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
mise sql # Sync database schema
|
mise //server:sql # Sync database schema
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debugging
|
### Debugging
|
||||||
|
|||||||
@@ -8,34 +8,42 @@ When contributing code through a pull request, please check the following:
|
|||||||
|
|
||||||
## Web Checks
|
## Web Checks
|
||||||
|
|
||||||
- [ ] `pnpm run lint` (linting via ESLint)
|
- [ ] `mise //web:lint` (linting via ESLint)
|
||||||
- [ ] `pnpm run format` (formatting via Prettier)
|
- [ ] `mise //web:format` (formatting via Prettier)
|
||||||
- [ ] `pnpm run check:svelte` (Type checking via SvelteKit)
|
- [ ] `mise //web:check-svelte` (type checking via SvelteKit)
|
||||||
- [ ] `pnpm run check:typescript` (check typescript)
|
- [ ] `mise //web:check-typescript` (type checking via `tsc`)
|
||||||
- [ ] `pnpm test` (unit tests)
|
- [ ] `mise //web:test` (unit tests)
|
||||||
|
|
||||||
:::tip AIO
|
:::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
|
## 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.
|
- [ ] 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
|
## Server Checks
|
||||||
|
|
||||||
- [ ] `pnpm run lint` (linting via ESLint)
|
- [ ] `mise //server:lint` (linting via ESLint)
|
||||||
- [ ] `pnpm run format` (formatting via Prettier)
|
- [ ] `mise //server:format` (formatting via Prettier)
|
||||||
- [ ] `pnpm run check` (Type checking via `tsc`)
|
- [ ] `mise //server:check` (type checking via `tsc`)
|
||||||
- [ ] `pnpm test` (unit tests)
|
- [ ] `mise //server:test` (unit tests)
|
||||||
|
|
||||||
:::tip AIO
|
:::tip AIO
|
||||||
Run all server checks with `pnpm run check:all`
|
Run all server checks with `mise //server:checklist`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
:::tip Auto Fix
|
:::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
|
## 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`.
|
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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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.
|
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
|
### Server and web apps
|
||||||
|
|
||||||
1. Clone the project repo.
|
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
|
#### Connect web to a remote backend
|
||||||
|
|
||||||
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
If you only want to do web development connected to an existing, remote backend, run from the repo root:
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```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:
|
If you're using PowerShell on Windows you may need to set the env var separately like so:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
|
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
|
||||||
pnpm run dev
|
mise //web:start
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `@immich/ui`
|
#### `@immich/ui`
|
||||||
@@ -90,20 +95,16 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
|||||||
|
|
||||||
#### Setup
|
#### Setup
|
||||||
|
|
||||||
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
|
1. Run `mise //mobile:install` to install Flutter dependencies.
|
||||||
2. Change to the immich (root) directory and trust the mise config with `mise trust`.
|
2. Run `mise //mobile:translation` to generate the translation file.
|
||||||
3. Install tools with mise: `mise install`.
|
3. Change to the `mobile/` directory and run `flutter run` to start the app.
|
||||||
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.
|
|
||||||
|
|
||||||
#### Translation
|
#### 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
|
```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.
|
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.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
### Unit tests
|
### Unit tests
|
||||||
|
|
||||||
Unit are run by calling `pnpm run test` from the `server/` directory.
|
Unit tests are run with `mise //server:test`.
|
||||||
You need to run `pnpm install` (in `server/`) before _once_.
|
You need to run `mise //server:install` before _once_.
|
||||||
|
|
||||||
### End to end tests
|
### End to end tests
|
||||||
|
|
||||||
@@ -17,8 +17,7 @@ make e2e
|
|||||||
|
|
||||||
Before you can run the tests, you need to run the following commands _once_:
|
Before you can run the tests, you need to run the following commands _once_:
|
||||||
|
|
||||||
- `pnpm install`
|
- `mise //e2e:ci-setup` (installs e2e, SDK, and CLI dependencies)
|
||||||
- `pnpm --filter @immich/sdk --filter @immich/cli build`
|
|
||||||
- `mise //:open-api`
|
- `mise //:open-api`
|
||||||
|
|
||||||
Once the test environment is running, the e2e tests can be run via:
|
Once the test environment is running, the e2e tests can be run via:
|
||||||
|
|||||||
@@ -154,33 +154,33 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
|
|
||||||
## Machine Learning
|
## Machine Learning
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| 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` | 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_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_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_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_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_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_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_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_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__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__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__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__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__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_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` | 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_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_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_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__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_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` | 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_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_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 |
|
| `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: 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
@@ -3,7 +3,7 @@ run = "pnpm install --filter documentation --frozen-lockfile"
|
|||||||
|
|
||||||
[tasks.start]
|
[tasks.start]
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "docusaurus --port 3005"
|
run = "docusaurus start --port 3005"
|
||||||
|
|
||||||
[tasks.build]
|
[tasks.build]
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
|
|||||||
@@ -83,9 +83,7 @@ volumes:
|
|||||||
model_cache:
|
model_cache:
|
||||||
prometheus_data:
|
prometheus_data:
|
||||||
grafana_data:
|
grafana_data:
|
||||||
pnpm_cache:
|
build_cache:
|
||||||
pnpm_store_server:
|
|
||||||
pnpm_store_web:
|
|
||||||
server_node_modules:
|
server_node_modules:
|
||||||
web_node_modules:
|
web_node_modules:
|
||||||
github_node_modules:
|
github_node_modules:
|
||||||
|
|||||||
+16
-1
@@ -1,11 +1,21 @@
|
|||||||
[tasks.install]
|
[tasks.install]
|
||||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||||
|
|
||||||
|
[tasks.build]
|
||||||
|
dir = "{{ config_root }}"
|
||||||
|
run = "docker compose build"
|
||||||
|
|
||||||
[tasks.test]
|
[tasks.test]
|
||||||
|
depends = ["//e2e:build", "//e2e:ci-setup"]
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "vitest --run"
|
run = "vitest --run"
|
||||||
|
|
||||||
|
[tasks.playwright-install]
|
||||||
|
env._.path = "./node_modules/.bin"
|
||||||
|
run = "playwright install"
|
||||||
|
|
||||||
[tasks."test-web"]
|
[tasks."test-web"]
|
||||||
|
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
||||||
env._.path = "./node_modules/.bin"
|
env._.path = "./node_modules/.bin"
|
||||||
run = "playwright test"
|
run = "playwright test"
|
||||||
|
|
||||||
@@ -30,7 +40,12 @@ run = "tsc --noEmit"
|
|||||||
|
|
||||||
|
|
||||||
[tasks.ci-setup]
|
[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" }
|
run = { task = ":install" }
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ describe('/server', () => {
|
|||||||
major: expect.any(Number),
|
major: expect.any(Number),
|
||||||
minor: expect.any(Number),
|
minor: expect.any(Number),
|
||||||
patch: expect.any(Number),
|
patch: expect.any(Number),
|
||||||
|
prerelease: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,18 +21,18 @@ describe('/system-config', () => {
|
|||||||
const response1 = await request(app)
|
const response1 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: false } });
|
.send({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||||
|
|
||||||
expect(response1.status).toBe(200);
|
expect(response1.status).toBe(200);
|
||||||
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
|
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false, channel: 'stable' } });
|
||||||
|
|
||||||
const response2 = await request(app)
|
const response2 = await request(app)
|
||||||
.put('/system-config')
|
.put('/system-config')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...config, newVersionCheck: { enabled: true } });
|
.send({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||||
|
|
||||||
expect(response2.status).toBe(200);
|
expect(response2.status).toBe(200);
|
||||||
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
|
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true, channel: 'stable' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an invalid config entry', async () => {
|
it('should reject an invalid config entry', async () => {
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
|||||||
result.duration.push(asset.duration);
|
result.duration.push(asset.duration);
|
||||||
result.projectionType.push(asset.projectionType);
|
result.projectionType.push(asset.projectionType);
|
||||||
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
||||||
result.city.push(asset.city);
|
result.city?.push(asset.city);
|
||||||
result.country.push(asset.country);
|
result.country?.push(asset.country);
|
||||||
result.visibility.push(asset.visibility);
|
result.visibility.push(asset.visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ test.describe('Timeline', () => {
|
|||||||
force: false,
|
force: false,
|
||||||
ids: [assetToTrash.id],
|
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 page.getByText('Trash', { exact: true }).click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||||
@@ -676,7 +676,7 @@ test.describe('Timeline', () => {
|
|||||||
ids: [assetToArchive.id],
|
ids: [assetToArchive.id],
|
||||||
});
|
});
|
||||||
await thumbnailUtils.expectThumbnailIsArchive(page, 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 page.getByRole('link').getByText('Archive').click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
||||||
@@ -823,7 +823,7 @@ test.describe('Timeline', () => {
|
|||||||
});
|
});
|
||||||
// ensure thumbnail still exists and has favorite icon
|
// ensure thumbnail still exists and has favorite icon
|
||||||
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
|
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 page.getByRole('link').getByText('Favorites').click();
|
||||||
await timelineUtils.waitForTimelineLoad(page);
|
await timelineUtils.waitForTimelineLoad(page);
|
||||||
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
|
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
|
||||||
|
|||||||
@@ -305,6 +305,8 @@
|
|||||||
"refreshing_all_libraries": "Refreshing all libraries",
|
"refreshing_all_libraries": "Refreshing all libraries",
|
||||||
"registration": "Admin Registration",
|
"registration": "Admin Registration",
|
||||||
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
"registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.",
|
||||||
|
"release_channel_release_candidate": "Release candidate",
|
||||||
|
"release_channel_stable": "Stable",
|
||||||
"remove_failed_jobs": "Remove failed jobs",
|
"remove_failed_jobs": "Remove failed jobs",
|
||||||
"require_password_change_on_login": "Require user to change password on first login",
|
"require_password_change_on_login": "Require user to change password on first login",
|
||||||
"reset_settings_to_default": "Reset settings to default",
|
"reset_settings_to_default": "Reset settings to default",
|
||||||
@@ -442,6 +444,8 @@
|
|||||||
"user_settings_description": "Manage user settings",
|
"user_settings_description": "Manage user settings",
|
||||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||||
"users_page_description": "Admin users page",
|
"users_page_description": "Admin users page",
|
||||||
|
"version_check_channel": "Release channel",
|
||||||
|
"version_check_channel_description": "Pick the release channel you want to get version announcements for",
|
||||||
"version_check_enabled_description": "Enable version check",
|
"version_check_enabled_description": "Enable version check",
|
||||||
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
"version_check_implications": "The version check feature relies on periodic communication with {server}",
|
||||||
"version_check_settings": "Version Check",
|
"version_check_settings": "Version Check",
|
||||||
@@ -698,6 +702,7 @@
|
|||||||
"birthdate_saved": "Date of birth saved successfully",
|
"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.",
|
"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",
|
"blurred_background": "Blurred background",
|
||||||
|
"browse_templates": "Browse templates",
|
||||||
"bugs_and_feature_requests": "Bugs & Feature Requests",
|
"bugs_and_feature_requests": "Bugs & Feature Requests",
|
||||||
"build": "Build",
|
"build": "Build",
|
||||||
"build_image": "Build Image",
|
"build_image": "Build Image",
|
||||||
@@ -839,6 +844,7 @@
|
|||||||
"copy_error": "Copy error",
|
"copy_error": "Copy error",
|
||||||
"copy_file_path": "Copy file path",
|
"copy_file_path": "Copy file path",
|
||||||
"copy_image": "Copy Image",
|
"copy_image": "Copy Image",
|
||||||
|
"copy_json": "Copy JSON",
|
||||||
"copy_link": "Copy link",
|
"copy_link": "Copy link",
|
||||||
"copy_link_to_clipboard": "Copy link to clipboard",
|
"copy_link_to_clipboard": "Copy link to clipboard",
|
||||||
"copy_password": "Copy password",
|
"copy_password": "Copy password",
|
||||||
@@ -976,7 +982,10 @@
|
|||||||
"downloading_asset_filename": "Downloading asset {filename}",
|
"downloading_asset_filename": "Downloading asset {filename}",
|
||||||
"downloading_from_icloud": "Downloading from iCloud",
|
"downloading_from_icloud": "Downloading from iCloud",
|
||||||
"downloading_media": "Downloading media",
|
"downloading_media": "Downloading media",
|
||||||
|
"drag_to_reorder": "Drag to reorder",
|
||||||
"drop_files_to_upload": "Drop files anywhere to upload",
|
"drop_files_to_upload": "Drop files anywhere to upload",
|
||||||
|
"duplicate": "Duplicate",
|
||||||
|
"duplicate_workflow": "Duplicate workflow",
|
||||||
"duplicates": "Duplicates",
|
"duplicates": "Duplicates",
|
||||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
@@ -2254,6 +2263,7 @@
|
|||||||
"step_delete_confirm": "Are you sure you want to delete this step?",
|
"step_delete_confirm": "Are you sure you want to delete this step?",
|
||||||
"step_details": "Step details",
|
"step_details": "Step details",
|
||||||
"steps": "Steps",
|
"steps": "Steps",
|
||||||
|
"steps_count": "{count, plural, one {# step} other {# steps}}",
|
||||||
"stop_casting": "Stop casting",
|
"stop_casting": "Stop casting",
|
||||||
"stop_motion_photo": "Stop Motion Photo",
|
"stop_motion_photo": "Stop Motion Photo",
|
||||||
"stop_photo_sharing": "Stop sharing your photos?",
|
"stop_photo_sharing": "Stop sharing your photos?",
|
||||||
@@ -2415,6 +2425,7 @@
|
|||||||
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
|
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
|
||||||
"use_current_connection": "Use current connection",
|
"use_current_connection": "Use current connection",
|
||||||
"use_custom_date_range": "Use custom date range instead",
|
"use_custom_date_range": "Use custom date range instead",
|
||||||
|
"use_template": "Use template",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"user_has_been_deleted": "This user has been deleted.",
|
"user_has_been_deleted": "This user has been deleted.",
|
||||||
"user_id": "User ID",
|
"user_id": "User ID",
|
||||||
@@ -2476,6 +2487,7 @@
|
|||||||
"week": "Week",
|
"week": "Week",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
|
"when": "When",
|
||||||
"width": "Width",
|
"width": "Width",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
"workflow": "Workflow",
|
"workflow": "Workflow",
|
||||||
@@ -2488,6 +2500,7 @@
|
|||||||
"workflow_name": "Workflow name",
|
"workflow_name": "Workflow name",
|
||||||
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
||||||
"workflow_summary": "Workflow summary",
|
"workflow_summary": "Workflow summary",
|
||||||
|
"workflow_templates": "Workflow templates",
|
||||||
"workflow_update_success": "Workflow updated successfully",
|
"workflow_update_success": "Workflow updated successfully",
|
||||||
"workflow_updated": "Workflow updated",
|
"workflow_updated": "Workflow updated",
|
||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from socket import socket
|
from socket import socket
|
||||||
|
|
||||||
from gunicorn.arbiter import Arbiter
|
from gunicorn.arbiter import Arbiter
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.logging import RichHandler
|
from rich.logging import RichHandler
|
||||||
@@ -42,6 +42,10 @@ class MaxBatchSize(BaseModel):
|
|||||||
ocr: int | None = None
|
ocr: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def default_worker_timeout() -> int:
|
||||||
|
return 900 if os.environ.get("DEVICE") == "rocm" else 300
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_prefix="MACHINE_LEARNING_",
|
env_prefix="MACHINE_LEARNING_",
|
||||||
@@ -54,7 +58,7 @@ class Settings(BaseSettings):
|
|||||||
model_ttl: int = 300
|
model_ttl: int = 300
|
||||||
model_ttl_poll_s: int = 10
|
model_ttl_poll_s: int = 10
|
||||||
workers: int = 1
|
workers: int = 1
|
||||||
worker_timeout: int = 300
|
worker_timeout: int = Field(default_factory=default_worker_timeout)
|
||||||
http_keepalive_timeout_s: int = 2
|
http_keepalive_timeout_s: int = 2
|
||||||
test_full: bool = False
|
test_full: bool = False
|
||||||
request_threads: int = os.cpu_count() or 4
|
request_threads: int = os.cpu_count() or 4
|
||||||
|
|||||||
@@ -89,4 +89,10 @@ class FaceRecognizer(InferenceModel):
|
|||||||
@property
|
@property
|
||||||
def _batch_size_default(self) -> int | None:
|
def _batch_size_default(self) -> int | None:
|
||||||
providers = ort.get_available_providers()
|
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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from threading import Lock
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -12,6 +13,37 @@ from immich_ml.schemas import ModelPrecision, SessionNode
|
|||||||
|
|
||||||
from ..config import log, settings
|
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:
|
class OrtSession:
|
||||||
session: ort.InferenceSession
|
session: ort.InferenceSession
|
||||||
@@ -48,7 +80,21 @@ class OrtSession:
|
|||||||
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
|
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
|
||||||
run_options: Any = None,
|
run_options: Any = None,
|
||||||
) -> list[NDArray[np.float32]]:
|
) -> 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
|
return outputs
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ dependencies = [
|
|||||||
"fastapi>=0.95.2,<1.0",
|
"fastapi>=0.95.2,<1.0",
|
||||||
"gunicorn>=21.1.0",
|
"gunicorn>=21.1.0",
|
||||||
"huggingface-hub>=1.0,<2.0",
|
"huggingface-hub>=1.0,<2.0",
|
||||||
"insightface>=0.7.3,<1.0",
|
"insightface>=0.7.3,<2.0",
|
||||||
"numpy>=2.4.0,<3.0",
|
"numpy>=2.4.0,<3.0",
|
||||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||||
"orjson>=3.9.5",
|
"orjson>=3.9.5",
|
||||||
|
|||||||
@@ -35,7 +35,37 @@ from immich_ml.sessions.ort import OrtSession
|
|||||||
from immich_ml.sessions.rknn import RknnSession, run_inference
|
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:
|
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:
|
def test_sets_default_cache_dir(self) -> None:
|
||||||
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
|
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
|
||||||
|
|
||||||
@@ -413,6 +443,52 @@ class TestOrtSession:
|
|||||||
|
|
||||||
assert sess_options is session.sess_options
|
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:
|
class TestAnnSession:
|
||||||
def test_creates_ann_session(self, ann_session: mock.Mock, info: mock.Mock) -> None:
|
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.load.assert_not_called()
|
||||||
onnx.save.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:
|
def test_set_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
||||||
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(facial_recognition=2))
|
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(facial_recognition=2))
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1004,7 +1004,7 @@ requires-dist = [
|
|||||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||||
{ name = "gunicorn", specifier = ">=21.1.0" },
|
{ name = "gunicorn", specifier = ">=21.1.0" },
|
||||||
{ name = "huggingface-hub", specifier = ">=1.0,<2.0" },
|
{ name = "huggingface-hub", specifier = ">=1.0,<2.0" },
|
||||||
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
|
{ name = "insightface", specifier = ">=0.7.3,<2.0" },
|
||||||
{ name = "numpy", specifier = ">=2.4.0,<3.0" },
|
{ name = "numpy", specifier = ">=2.4.0,<3.0" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
|
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||||
|
|
||||||
[[tools."aqua:flutter/flutter"]]
|
[[tools."aqua:flutter/flutter"]]
|
||||||
version = "3.41.9"
|
version = "3.44.0"
|
||||||
backend = "aqua:flutter/flutter"
|
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]]
|
[[tools.flutter]]
|
||||||
version = "3.41.9-stable"
|
version = "3.41.9-stable"
|
||||||
backend = "asdf:flutter"
|
backend = "asdf:flutter"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ config_roots = [
|
|||||||
|
|
||||||
[tools]
|
[tools]
|
||||||
node = "24.15.0"
|
node = "24.15.0"
|
||||||
"aqua:flutter/flutter" = "3.41.9"
|
"aqua:flutter/flutter" = "3.44.0"
|
||||||
pnpm = "10.33.4"
|
pnpm = "10.33.4"
|
||||||
terragrunt = "1.0.3"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.6"
|
opentofu = "1.11.6"
|
||||||
@@ -73,7 +73,6 @@ run = "bash ./bin/generate-dart-sdk.sh"
|
|||||||
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
||||||
run = [
|
run = [
|
||||||
{ task = "//:plugins" },
|
{ task = "//:plugins" },
|
||||||
{ task = "//server:build" },
|
|
||||||
{ task = "//server:install" },
|
{ task = "//server:install" },
|
||||||
{ task = "//server:build" },
|
{ task = "//server:build" },
|
||||||
{ task = "//server:sync-open-api" },
|
{ task = "//server:sync-open-api" },
|
||||||
@@ -85,6 +84,72 @@ run = [
|
|||||||
dir = "server"
|
dir = "server"
|
||||||
run = "node ./dist/bin/sync-sql.js"
|
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 --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
|
# SDK tasks
|
||||||
[tasks."sdk:install"]
|
[tasks."sdk:install"]
|
||||||
dir = "packages/sdk"
|
dir = "packages/sdk"
|
||||||
@@ -100,3 +165,14 @@ run = "pnpm format"
|
|||||||
|
|
||||||
[tasks."i18n:format-fix"]
|
[tasks."i18n:format-fix"]
|
||||||
run = "pnpm 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" },
|
||||||
|
]
|
||||||
|
|||||||
Vendored
-1
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
|
|
||||||
"dart.lineLength": 120,
|
"dart.lineLength": 120,
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
"editor.rulers": [
|
"editor.rulers": [
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import app.alextran.immich.images.LocalImageApi
|
|||||||
import app.alextran.immich.images.LocalImagesImpl
|
import app.alextran.immich.images.LocalImagesImpl
|
||||||
import app.alextran.immich.images.RemoteImageApi
|
import app.alextran.immich.images.RemoteImageApi
|
||||||
import app.alextran.immich.images.RemoteImagesImpl
|
import app.alextran.immich.images.RemoteImagesImpl
|
||||||
|
import app.alextran.immich.permission.PermissionApi
|
||||||
|
import app.alextran.immich.permission.PermissionApiImpl
|
||||||
import app.alextran.immich.sync.NativeSyncApi
|
import app.alextran.immich.sync.NativeSyncApi
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl26
|
import app.alextran.immich.sync.NativeSyncApiImpl26
|
||||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||||
@@ -44,7 +46,9 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
} else {
|
} else {
|
||||||
NativeSyncApiImpl30(ctx)
|
NativeSyncApiImpl30(ctx)
|
||||||
}
|
}
|
||||||
|
val permissionApiImpl = PermissionApiImpl(ctx)
|
||||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||||
|
PermissionApi.setUp(messenger, permissionApiImpl)
|
||||||
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
||||||
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
||||||
|
|
||||||
@@ -53,6 +57,7 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||||
|
flutterEngine.plugins.add(permissionApiImpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
||||||
@@ -60,6 +65,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
|
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
|
||||||
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
|
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
|
||||||
nativeApi?.detachFromEngine()
|
nativeApi?.detachFromEngine()
|
||||||
|
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
|
||||||
|
permissionApi?.detachFromEngine()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
|
|
||||||
|
class ManageMediaPermissionDelegate(
|
||||||
|
context: Context,
|
||||||
|
private val requestCode: Int = 1003,
|
||||||
|
) : PluginRegistry.ActivityResultListener {
|
||||||
|
private val ctx = context.applicationContext
|
||||||
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
|
||||||
|
|
||||||
|
fun hasManageMediaPermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
MediaStore.canManageMedia(ctx)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (hasManageMediaPermission()) {
|
||||||
|
callback(Result.success(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openManageMediaPermissionSettings(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
openManageMediaPermissionSettings(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openManageMediaPermissionSettings(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||||
|
callback(Result.success(false))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val activity = activityBinding?.activity
|
||||||
|
if (activity == null) {
|
||||||
|
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingResult = callback
|
||||||
|
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA).apply {
|
||||||
|
data = "package:${activity.packageName}".toUri()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingResult = null
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError("ACTIVITY_LAUNCH_FAILED", "Failed to launch MANAGE_MEDIA settings", e.toString())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activityBinding = binding
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetachedFromActivity() {
|
||||||
|
failPending("ACTIVITY_DETACHED", "Activity detached before MANAGE_MEDIA result")
|
||||||
|
activityBinding?.removeActivityResultListener(this)
|
||||||
|
activityBinding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode == this.requestCode) {
|
||||||
|
val callback = pendingResult
|
||||||
|
pendingResult = null
|
||||||
|
callback?.invoke(Result.success(hasManageMediaPermission()))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failPending(code: String, message: String) {
|
||||||
|
val callback = pendingResult ?: return
|
||||||
|
pendingResult = null
|
||||||
|
callback(Result.failure(FlutterError(code, message, null)))
|
||||||
|
}
|
||||||
|
}
|
||||||
+128
@@ -0,0 +1,128 @@
|
|||||||
|
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||||
|
|
||||||
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import io.flutter.plugin.common.BasicMessageChannel
|
||||||
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
|
import io.flutter.plugin.common.MessageCodec
|
||||||
|
import io.flutter.plugin.common.StandardMethodCodec
|
||||||
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
private object PermissionApiPigeonUtils {
|
||||||
|
|
||||||
|
fun wrapResult(result: Any?): List<Any?> {
|
||||||
|
return listOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wrapError(exception: Throwable): List<Any?> {
|
||||||
|
return if (exception is FlutterError) {
|
||||||
|
listOf(
|
||||||
|
exception.code,
|
||||||
|
exception.message,
|
||||||
|
exception.details
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
exception.javaClass.simpleName,
|
||||||
|
exception.toString(),
|
||||||
|
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||||
|
* @property code The error code.
|
||||||
|
* @property message The error message.
|
||||||
|
* @property details The error details. Must be a datatype supported by the api codec.
|
||||||
|
*/
|
||||||
|
class FlutterError (
|
||||||
|
val code: String,
|
||||||
|
override val message: String? = null,
|
||||||
|
val details: Any? = null
|
||||||
|
) : RuntimeException()
|
||||||
|
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
|
||||||
|
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||||
|
return super.readValueOfType(type, buffer)
|
||||||
|
}
|
||||||
|
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||||
|
super.writeValue(stream, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||||
|
interface PermissionApi {
|
||||||
|
fun hasManageMediaPermission(): Boolean
|
||||||
|
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
|
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The codec used by PermissionApi. */
|
||||||
|
val codec: MessageCodec<Any?> by lazy {
|
||||||
|
PermissionApiPigeonCodec()
|
||||||
|
}
|
||||||
|
/** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */
|
||||||
|
@JvmOverloads
|
||||||
|
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
|
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
listOf(api.hasManageMediaPermission())
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
PermissionApiPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.requestManageMediaPermission{ result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { _, reply ->
|
||||||
|
api.manageMediaPermission{ result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(PermissionApiPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package app.alextran.immich.permission
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import app.alextran.immich.core.ImmichPlugin
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
|
||||||
|
class PermissionApiImpl(context: Context) : ImmichPlugin(), PermissionApi, ActivityAware {
|
||||||
|
private val manageMediaPermissionDelegate = ManageMediaPermissionDelegate(context)
|
||||||
|
|
||||||
|
override fun hasManageMediaPermission(): Boolean =
|
||||||
|
manageMediaPermissionDelegate.hasManageMediaPermission()
|
||||||
|
|
||||||
|
override fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
manageMediaPermissionDelegate.requestManageMediaPermission { completeWhenActive(callback, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||||
|
manageMediaPermissionDelegate.manageMediaPermission { completeWhenActive(callback, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
manageMediaPermissionDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
manageMediaPermissionDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
manageMediaPermissionDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
manageMediaPermissionDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package app.alextran.immich.sync
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.ContentUris
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
|
import io.flutter.plugin.common.PluginRegistry
|
||||||
|
|
||||||
|
class MediaTrashDelegate(
|
||||||
|
context: Context,
|
||||||
|
private val trashRequestCode: Int = 1002,
|
||||||
|
) : PluginRegistry.ActivityResultListener {
|
||||||
|
private val ctx = context.applicationContext
|
||||||
|
private var activityBinding: ActivityPluginBinding? = null
|
||||||
|
private var pendingResult: ((Result<Boolean>) -> Unit)? = null
|
||||||
|
|
||||||
|
private fun hasManageMediaPermission(): Boolean {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
MediaStore.canManageMedia(ctx)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R || !hasManageMediaPermission()) {
|
||||||
|
callback(Result.failure(FlutterError("PERMISSION_DENIED", "Media permission required", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val id = mediaId.toLongOrNull()
|
||||||
|
if (id == null) {
|
||||||
|
callback(Result.failure(FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isInTrash(id)) {
|
||||||
|
callback(Result.failure(FlutterError("TRASH_NOT_FOUND", "Item with id=$id not found in trash", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreUri(ContentUris.withAppendedId(contentUriForType(type.toInt()), id), callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun restoreUri(
|
||||||
|
contentUri: Uri,
|
||||||
|
callback: (Result<Boolean>) -> Unit,
|
||||||
|
) {
|
||||||
|
val activity = activityBinding?.activity
|
||||||
|
if (activity == null) {
|
||||||
|
callback(Result.failure(FlutterError("NO_ACTIVITY", "Activity not available", null)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, listOf(contentUri), false)
|
||||||
|
pendingResult = callback
|
||||||
|
activity.startIntentSenderForResult(
|
||||||
|
pendingIntent.intentSender,
|
||||||
|
trashRequestCode,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
pendingResult = null
|
||||||
|
callback(
|
||||||
|
Result.failure(
|
||||||
|
FlutterError("TRASH_ERROR", "Error creating or starting trash request", e.toString())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
private fun isInTrash(id: Long): Boolean {
|
||||||
|
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
val args = Bundle().apply {
|
||||||
|
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
|
||||||
|
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
|
||||||
|
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
|
||||||
|
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
|
||||||
|
}
|
||||||
|
return ctx.contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
|
||||||
|
?.use { it.moveToFirst() } == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun contentUriForType(type: Int): Uri =
|
||||||
|
when (type) {
|
||||||
|
// Same order as AssetType from Dart.
|
||||||
|
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||||
|
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
|
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||||
|
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
activityBinding = binding
|
||||||
|
binding.addActivityResultListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDetachedFromActivity() {
|
||||||
|
failPending("ACTIVITY_DETACHED", "Activity detached before trash result")
|
||||||
|
activityBinding?.removeActivityResultListener(this)
|
||||||
|
activityBinding = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||||
|
if (requestCode == trashRequestCode) {
|
||||||
|
val callback = pendingResult
|
||||||
|
pendingResult = null
|
||||||
|
callback?.invoke(Result.success(resultCode == Activity.RESULT_OK))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failPending(code: String, message: String) {
|
||||||
|
val callback = pendingResult ?: return
|
||||||
|
pendingResult = null
|
||||||
|
callback(Result.failure(FlutterError(code, message, null)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -553,6 +553,7 @@ interface NativeSyncApi {
|
|||||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||||
fun cancelHashing()
|
fun cancelHashing()
|
||||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||||
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -747,6 +748,27 @@ interface NativeSyncApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val mediaIdArg = args[0] as String
|
||||||
|
val typeArg = args[1] as Long
|
||||||
|
api.restoreFromTrashById(mediaIdArg, typeArg) { result: Result<Boolean> ->
|
||||||
|
val error = result.exceptionOrNull()
|
||||||
|
if (error != null) {
|
||||||
|
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||||
|
} else {
|
||||||
|
val data = result.getOrNull()
|
||||||
|
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
run {
|
run {
|
||||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$separatedMessageChannelSuffix", codec, taskQueue)
|
||||||
if (api != null) {
|
if (api != null) {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import com.bumptech.glide.Glide
|
|||||||
import com.bumptech.glide.load.ImageHeaderParser
|
import com.bumptech.glide.load.ImageHeaderParser
|
||||||
import com.bumptech.glide.load.ImageHeaderParserUtils
|
import com.bumptech.glide.load.ImageHeaderParserUtils
|
||||||
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
|
import com.bumptech.glide.load.resource.bitmap.DefaultImageHeaderParser
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||||
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -39,10 +41,11 @@ sealed class AssetResult {
|
|||||||
private const val TAG = "NativeSyncApiImplBase"
|
private const val TAG = "NativeSyncApiImplBase"
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
@SuppressLint("InlinedApi")
|
||||||
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAware {
|
||||||
private val ctx: Context = context.applicationContext
|
private val ctx: Context = context.applicationContext
|
||||||
|
|
||||||
private var hashTask: Job? = null
|
private var hashTask: Job? = null
|
||||||
|
private val mediaTrashDelegate = MediaTrashDelegate(ctx)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
private const val MAX_CONCURRENT_HASH_OPERATIONS = 16
|
||||||
@@ -448,6 +451,26 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
|
|||||||
hashTask = null
|
hashTask = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||||
|
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||||
|
mediaTrashDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivityForConfigChanges() {
|
||||||
|
mediaTrashDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||||
|
mediaTrashDelegate.onAttachedToActivity(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromActivity() {
|
||||||
|
mediaTrashDelegate.onDetachedFromActivity()
|
||||||
|
}
|
||||||
|
|
||||||
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
|
// This method is only implemented on iOS; on Android, we do not have a concept of cloud IDs
|
||||||
@Suppress("unused", "UNUSED_PARAMETER")
|
@Suppress("unused", "UNUSED_PARAMETER")
|
||||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult> {
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ android.nonTransitiveRClass=false
|
|||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
org.gradle.caching=true
|
org.gradle.caching=true
|
||||||
org.gradle.parallel=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
|
||||||
|
|||||||
@@ -1,58 +1,23 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- background_downloader (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- bonsoir_darwin (0.0.1):
|
- bonsoir_darwin (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- connectivity_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- cupertino_http (0.0.1):
|
- cupertino_http (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- device_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_local_notifications (0.0.1):
|
- flutter_local_notifications (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
|
||||||
- Flutter
|
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- KeychainAccess
|
|
||||||
- flutter_web_auth_2 (5.0.0):
|
|
||||||
- Flutter
|
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
- geolocator_apple (1.2.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- home_widget (0.0.1):
|
- home_widget (0.0.1):
|
||||||
- Flutter
|
- 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):
|
- native_video_player (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- network_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- package_info_plus (0.4.5):
|
|
||||||
- Flutter
|
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- photo_manager (3.9.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- share_handler_ios (0.0.14):
|
- share_handler_ios (0.0.14):
|
||||||
- Flutter
|
- Flutter
|
||||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||||
@@ -61,144 +26,56 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- share_handler_ios_models
|
- share_handler_ios_models
|
||||||
- share_handler_ios_models (0.0.9)
|
- 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:
|
DEPENDENCIES:
|
||||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
|
||||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
|
||||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- 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_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`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
|
||||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
- 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`)
|
- 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`)
|
- 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 (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
- 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:
|
EXTERNAL SOURCES:
|
||||||
background_downloader:
|
|
||||||
:path: ".symlinks/plugins/background_downloader/ios"
|
|
||||||
bonsoir_darwin:
|
bonsoir_darwin:
|
||||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||||
connectivity_plus:
|
|
||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
|
||||||
cupertino_http:
|
cupertino_http:
|
||||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||||
device_info_plus:
|
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_local_notifications:
|
flutter_local_notifications:
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
: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:
|
fluttertoast:
|
||||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||||
geolocator_apple:
|
|
||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
|
||||||
home_widget:
|
home_widget:
|
||||||
:path: ".symlinks/plugins/home_widget/ios"
|
: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:
|
native_video_player:
|
||||||
:path: ".symlinks/plugins/native_video_player/ios"
|
: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:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
|
||||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
|
||||||
share_handler_ios:
|
share_handler_ios:
|
||||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||||
share_handler_ios_models:
|
share_handler_ios_models:
|
||||||
:path: ".symlinks/plugins/share_handler_ios/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:
|
SPEC CHECKSUMS:
|
||||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
|
||||||
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
||||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
|
||||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
|
||||||
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
|
|
||||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
|
||||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
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
|
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
|
||||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
|
||||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
|
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */; };
|
||||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */; };
|
||||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */; };
|
||||||
|
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */; };
|
||||||
|
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */; };
|
||||||
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */; };
|
||||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
||||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
||||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
|
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -105,6 +108,8 @@
|
|||||||
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
B21E34AB2E5B09100031FDB9 /* BackgroundWorker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.swift; sourceTree = "<group>"; };
|
||||||
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
B25D37782E72CA15008B6CA7 /* Connectivity.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connectivity.g.swift; sourceTree = "<group>"; };
|
||||||
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
B25D377B2E72CA20008B6CA7 /* ConnectivityApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityApiImpl.swift; sourceTree = "<group>"; };
|
||||||
|
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApi.g.swift; sourceTree = "<group>"; };
|
||||||
|
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionApiImpl.swift; sourceTree = "<group>"; };
|
||||||
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
B2BE315E2E5E5229006EEF88 /* BackgroundWorker.g.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundWorker.g.swift; sourceTree = "<group>"; };
|
||||||
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
F0B57D382DF764BD00DC5BCC /* WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -125,6 +130,7 @@
|
|||||||
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
@@ -189,6 +195,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
||||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
||||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
||||||
@@ -243,6 +250,7 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -283,6 +291,7 @@
|
|||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
|
B2EE00052E72CA15008B6CA7 /* Permission */,
|
||||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
@@ -317,6 +326,15 @@
|
|||||||
path = Connectivity;
|
path = Connectivity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
B2EE00052E72CA15008B6CA7 /* Permission */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */,
|
||||||
|
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */,
|
||||||
|
);
|
||||||
|
path = Permission;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -346,6 +364,9 @@
|
|||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
@@ -449,6 +470,7 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||||
);
|
);
|
||||||
@@ -619,6 +641,8 @@
|
|||||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||||
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
|
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.swift in Sources */,
|
||||||
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
B25D377A2E72CA15008B6CA7 /* Connectivity.g.swift in Sources */,
|
||||||
|
B2EE00022E72CA15008B6CA7 /* PermissionApi.g.swift in Sources */,
|
||||||
|
B2EE00042E72CA15008B6CA7 /* PermissionApiImpl.swift in Sources */,
|
||||||
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
|
FE5499F82F1198E2006016CB /* RemoteImagesImpl.swift in Sources */,
|
||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
||||||
@@ -1269,7 +1293,17 @@
|
|||||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
||||||
productName = StructuredFieldValues;
|
productName = StructuredFieldValues;
|
||||||
};
|
};
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* 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 */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-2
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -19,6 +18,24 @@
|
|||||||
"version" : "7.8.0"
|
"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",
|
"identity" : "sqlite-data",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -146,5 +163,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,24 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Immich.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -19,6 +18,24 @@
|
|||||||
"version" : "7.9.0"
|
"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",
|
"identity" : "sqlite-data",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -146,5 +163,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 3
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import native_video_player
|
|||||||
|
|
||||||
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
||||||
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
||||||
|
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
|
||||||
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
||||||
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
||||||
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
|
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
|
||||||
|
|||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
import Flutter
|
||||||
|
#elseif os(macOS)
|
||||||
|
import FlutterMacOS
|
||||||
|
#else
|
||||||
|
#error("Unsupported platform.")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||||
|
return [result]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func wrapError(_ error: Any) -> [Any?] {
|
||||||
|
if let pigeonError = error as? PigeonError {
|
||||||
|
return [
|
||||||
|
pigeonError.code,
|
||||||
|
pigeonError.message,
|
||||||
|
pigeonError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if let flutterError = error as? FlutterError {
|
||||||
|
return [
|
||||||
|
flutterError.code,
|
||||||
|
flutterError.message,
|
||||||
|
flutterError.details,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
"\(error)",
|
||||||
|
"\(Swift.type(of: error))",
|
||||||
|
"Stacktrace: \(Thread.callStackSymbols)",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isNullish(_ value: Any?) -> Bool {
|
||||||
|
return value is NSNull || value == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
return value as! T?
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||||
|
protocol PermissionApi {
|
||||||
|
func hasManageMediaPermission() throws -> Bool
|
||||||
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||||
|
class PermissionApiSetup {
|
||||||
|
static var codec: FlutterStandardMessageCodec { FlutterStandardMessageCodec.sharedInstance() }
|
||||||
|
/// Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`.
|
||||||
|
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
|
||||||
|
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||||
|
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
hasManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
do {
|
||||||
|
let result = try api.hasManageMediaPermission()
|
||||||
|
reply(wrapResult(result))
|
||||||
|
} catch {
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hasManageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let requestManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
requestManageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
api.requestManageMediaPermission { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
requestManageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
let manageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
manageMediaPermissionChannel.setMessageHandler { _, reply in
|
||||||
|
api.manageMediaPermission { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manageMediaPermissionChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
class PermissionApiImpl: PermissionApi {
|
||||||
|
func hasManageMediaPermission() throws -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(.success(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(.success(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+19
@@ -537,6 +537,7 @@ protocol NativeSyncApi {
|
|||||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||||
func cancelHashing() throws
|
func cancelHashing() throws
|
||||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||||
|
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,6 +722,24 @@ class NativeSyncApiSetup {
|
|||||||
} else {
|
} else {
|
||||||
getTrashedAssetsChannel.setMessageHandler(nil)
|
getTrashedAssetsChannel.setMessageHandler(nil)
|
||||||
}
|
}
|
||||||
|
let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
|
if let api = api {
|
||||||
|
restoreFromTrashByIdChannel.setMessageHandler { message, reply in
|
||||||
|
let args = message as! [Any?]
|
||||||
|
let mediaIdArg = args[0] as! String
|
||||||
|
let typeArg = args[1] as! Int64
|
||||||
|
api.restoreFromTrashById(mediaId: mediaIdArg, type: typeArg) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let res):
|
||||||
|
reply(wrapResult(res))
|
||||||
|
case .failure(let error):
|
||||||
|
reply(wrapError(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
restoreFromTrashByIdChannel.setMessageHandler(nil)
|
||||||
|
}
|
||||||
let getCloudIdForAssetIdsChannel = taskQueue == nil
|
let getCloudIdForAssetIdsChannel = taskQueue == nil
|
||||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||||
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
: FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec, taskQueue: taskQueue)
|
||||||
|
|||||||
@@ -382,6 +382,10 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||||||
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
|
func getTrashedAssets() throws -> [String: [PlatformAsset]] {
|
||||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
|
completion(.success(false))
|
||||||
|
}
|
||||||
|
|
||||||
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||||
// Ensure to actually getting all assets for the Recents album
|
// Ensure to actually getting all assets for the Recents album
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ const String kBackupLivePhotoGroup = 'backup_live_photo_group';
|
|||||||
const String kDownloadGroupImage = 'group_image';
|
const String kDownloadGroupImage = 'group_image';
|
||||||
const String kDownloadGroupVideo = 'group_video';
|
const String kDownloadGroupVideo = 'group_video';
|
||||||
const String kDownloadGroupLivePhoto = 'group_livephoto';
|
const String kDownloadGroupLivePhoto = 'group_livephoto';
|
||||||
|
const String kShareDownloadGroup = 'group_share';
|
||||||
|
|
||||||
// Timeline constants
|
// Timeline constants
|
||||||
const int kTimelineNoneSegmentSize = 120;
|
const int kTimelineNoneSegmentSize = 120;
|
||||||
|
|||||||
@@ -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/album_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/backup_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/cleanup_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/image_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/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/slideshow_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/theme_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/timeline_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/viewer_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/metadata_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 {
|
class AppConfig {
|
||||||
|
final LogLevel logLevel;
|
||||||
final ThemeConfig theme;
|
final ThemeConfig theme;
|
||||||
final CleanupConfig cleanup;
|
final CleanupConfig cleanup;
|
||||||
final MapConfig map;
|
final MapConfig map;
|
||||||
@@ -18,8 +29,10 @@ class AppConfig {
|
|||||||
final SlideshowConfig slideshow;
|
final SlideshowConfig slideshow;
|
||||||
final AlbumConfig album;
|
final AlbumConfig album;
|
||||||
final BackupConfig backup;
|
final BackupConfig backup;
|
||||||
|
final NetworkConfig network;
|
||||||
|
|
||||||
const AppConfig({
|
const AppConfig({
|
||||||
|
this.logLevel = .info,
|
||||||
this.theme = const .new(),
|
this.theme = const .new(),
|
||||||
this.cleanup = const .new(),
|
this.cleanup = const .new(),
|
||||||
this.map = const .new(),
|
this.map = const .new(),
|
||||||
@@ -29,9 +42,11 @@ class AppConfig {
|
|||||||
this.slideshow = const .new(),
|
this.slideshow = const .new(),
|
||||||
this.album = const .new(),
|
this.album = const .new(),
|
||||||
this.backup = const .new(),
|
this.backup = const .new(),
|
||||||
|
this.network = const .new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
AppConfig copyWith({
|
AppConfig copyWith({
|
||||||
|
LogLevel? logLevel,
|
||||||
ThemeConfig? theme,
|
ThemeConfig? theme,
|
||||||
CleanupConfig? cleanup,
|
CleanupConfig? cleanup,
|
||||||
MapConfig? map,
|
MapConfig? map,
|
||||||
@@ -41,7 +56,9 @@ class AppConfig {
|
|||||||
SlideshowConfig? slideshow,
|
SlideshowConfig? slideshow,
|
||||||
AlbumConfig? album,
|
AlbumConfig? album,
|
||||||
BackupConfig? backup,
|
BackupConfig? backup,
|
||||||
|
NetworkConfig? network,
|
||||||
}) => .new(
|
}) => .new(
|
||||||
|
logLevel: logLevel ?? this.logLevel,
|
||||||
theme: theme ?? this.theme,
|
theme: theme ?? this.theme,
|
||||||
cleanup: cleanup ?? this.cleanup,
|
cleanup: cleanup ?? this.cleanup,
|
||||||
map: map ?? this.map,
|
map: map ?? this.map,
|
||||||
@@ -51,12 +68,14 @@ class AppConfig {
|
|||||||
slideshow: slideshow ?? this.slideshow,
|
slideshow: slideshow ?? this.slideshow,
|
||||||
album: album ?? this.album,
|
album: album ?? this.album,
|
||||||
backup: backup ?? this.backup,
|
backup: backup ?? this.backup,
|
||||||
|
network: network ?? this.network,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is AppConfig &&
|
(other is AppConfig &&
|
||||||
|
other.logLevel == logLevel &&
|
||||||
other.theme == theme &&
|
other.theme == theme &&
|
||||||
other.cleanup == cleanup &&
|
other.cleanup == cleanup &&
|
||||||
other.map == map &&
|
other.map == map &&
|
||||||
@@ -65,12 +84,118 @@ class AppConfig {
|
|||||||
other.viewer == viewer &&
|
other.viewer == viewer &&
|
||||||
other.slideshow == slideshow &&
|
other.slideshow == slideshow &&
|
||||||
other.album == album &&
|
other.album == album &&
|
||||||
other.backup == backup);
|
other.backup == backup &&
|
||||||
|
other.network == network);
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
String toString() =>
|
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>(MetadataKey<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<MetadataKey<Object>, Object> entries) {
|
||||||
|
var config = const AppConfig();
|
||||||
|
for (final MapEntry(key: key, value: value) in entries.entries) {
|
||||||
|
config = config.write(key, value);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig write<T extends Object>(MetadataKey<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 {
|
class NetworkConfig {
|
||||||
final bool autoEndpointSwitching;
|
final bool autoEndpointSwitching;
|
||||||
final String? preferredWifiName;
|
final String preferredWifiName;
|
||||||
final String? localEndpoint;
|
final String localEndpoint;
|
||||||
final List<String> externalEndpointList;
|
final List<String> externalEndpointList;
|
||||||
final Map<String, String> customHeaders;
|
final Map<String, String> customHeaders;
|
||||||
|
|
||||||
const NetworkConfig({
|
const NetworkConfig({
|
||||||
this.autoEndpointSwitching = false,
|
this.autoEndpointSwitching = false,
|
||||||
this.preferredWifiName,
|
this.preferredWifiName = '',
|
||||||
this.localEndpoint,
|
this.localEndpoint = '',
|
||||||
this.externalEndpointList = const [],
|
this.externalEndpointList = const [],
|
||||||
this.customHeaders = 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)';
|
|
||||||
}
|
|
||||||
@@ -1,142 +1,105 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/colors.dart';
|
import 'package:immich_mobile/constants/colors.dart';
|
||||||
import 'package:immich_mobile/constants/enums.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/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||||
|
|
||||||
enum MetadataDomain<T extends Object> {
|
enum MetadataScope {
|
||||||
appConfig<AppConfig>('config.app'),
|
user, // keys with this scope are deleted on logout
|
||||||
systemConfig<SystemConfig>('config.system');
|
system;
|
||||||
|
|
||||||
final String prefix;
|
const MetadataScope();
|
||||||
const MetadataDomain(this.prefix);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MetadataKey<T extends Object> {
|
enum MetadataKey<T extends Object> {
|
||||||
// Theme
|
// Theme
|
||||||
themePrimaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
|
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
||||||
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
|
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||||
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
themeDynamic<bool>(),
|
||||||
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
|
themeColorfulInterface<bool>(),
|
||||||
|
|
||||||
// Image
|
// Image
|
||||||
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
|
imagePreferRemote<bool>(),
|
||||||
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
|
imageLoadOriginal<bool>(),
|
||||||
|
|
||||||
// Viewer
|
// Viewer
|
||||||
viewerLoopVideo<bool>(.appConfig, 'viewer.loopVideo', true),
|
viewerLoopVideo<bool>(),
|
||||||
viewerLoadOriginalVideo<bool>(.appConfig, 'viewer.loadOriginalVideo', false),
|
viewerLoadOriginalVideo<bool>(),
|
||||||
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
|
viewerAutoPlayVideo<bool>(),
|
||||||
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
|
viewerTapToNavigate<bool>(),
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
|
networkAutoEndpointSwitching<bool>(scope: .system),
|
||||||
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
|
networkPreferredWifiName<String>(scope: .system),
|
||||||
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
|
networkLocalEndpoint<String>(scope: .system),
|
||||||
networkExternalEndpointList<List<String>>(
|
networkExternalEndpointList<List<String>>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)),
|
||||||
.systemConfig,
|
|
||||||
'network.externalEndpointList',
|
|
||||||
[],
|
|
||||||
_ListCodec(_PrimitiveCodec.string),
|
|
||||||
),
|
|
||||||
networkCustomHeaders<Map<String, String>>(
|
networkCustomHeaders<Map<String, String>>(
|
||||||
.systemConfig,
|
scope: .system,
|
||||||
'network.customHeaders',
|
codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||||
{},
|
|
||||||
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
albumSortMode<AlbumSortMode>(
|
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
||||||
.appConfig,
|
albumIsReverse<bool>(),
|
||||||
'album.sortMode',
|
albumIsGrid<bool>(),
|
||||||
AlbumSortMode.mostRecent,
|
|
||||||
_EnumCodec(AlbumSortMode.values),
|
|
||||||
),
|
|
||||||
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
|
|
||||||
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
|
|
||||||
|
|
||||||
// Backup
|
// Backup
|
||||||
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
|
backupEnabled<bool>(),
|
||||||
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
|
backupUseCellularForVideos<bool>(),
|
||||||
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
|
backupUseCellularForPhotos<bool>(),
|
||||||
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
|
backupRequireCharging<bool>(),
|
||||||
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
|
backupTriggerDelay<int>(),
|
||||||
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
|
backupSyncAlbums<bool>(),
|
||||||
|
|
||||||
// Timeline
|
// Timeline
|
||||||
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
timelineTilesPerRow<int>(),
|
||||||
timelineGroupAssetsBy<GroupAssetsBy>(
|
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
|
||||||
.appConfig,
|
timelineStorageIndicator<bool>(),
|
||||||
'timeline.groupAssetsBy',
|
|
||||||
GroupAssetsBy.day,
|
|
||||||
_EnumCodec(GroupAssetsBy.values),
|
|
||||||
),
|
|
||||||
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
|
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
logLevel<LogLevel>(scope: .system, codec: _EnumCodec(LogLevel.values)),
|
||||||
|
|
||||||
// Map
|
// Map
|
||||||
mapShowFavoriteOnly<bool>(.appConfig, 'map.showFavoriteOnly', false),
|
mapShowFavoriteOnly<bool>(),
|
||||||
mapRelativeDate<int>(.appConfig, 'map.relativeDate', 0),
|
mapRelativeDate<int>(),
|
||||||
mapIncludeArchived<bool>(.appConfig, 'map.includeArchived', false),
|
mapIncludeArchived<bool>(),
|
||||||
mapThemeMode<ThemeMode>(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
|
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||||
mapWithPartners<bool>(.appConfig, 'map.withPartners', false),
|
mapWithPartners<bool>(),
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
|
cleanupKeepFavorites<bool>(),
|
||||||
cleanupKeepMediaType<AssetKeepType>(
|
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
|
||||||
.appConfig,
|
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||||
'cleanup.keepMediaType',
|
cleanupCutoffDaysAgo<int>(),
|
||||||
AssetKeepType.none,
|
cleanupDefaultsInitialized<bool>(),
|
||||||
_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
|
// Slideshow
|
||||||
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
|
slideshowTransition<bool>(),
|
||||||
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
|
slideshowRepeat<bool>(),
|
||||||
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
|
slideshowDuration<int>(),
|
||||||
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
|
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
||||||
slideshowDirection<SlideshowDirection>(
|
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
||||||
.appConfig,
|
|
||||||
'slideshow.direction',
|
|
||||||
SlideshowDirection.forward,
|
|
||||||
_EnumCodec(SlideshowDirection.values),
|
|
||||||
);
|
|
||||||
|
|
||||||
final MetadataDomain domain;
|
final MetadataScope scope;
|
||||||
final String name;
|
|
||||||
final T defaultValue;
|
|
||||||
final _MetadataCodec<T>? _codecOverride;
|
final _MetadataCodec<T>? _codecOverride;
|
||||||
|
|
||||||
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
|
const MetadataKey({this.scope = .user, _MetadataCodec<T>? codec}) : _codecOverride = codec;
|
||||||
|
|
||||||
String get key => '${domain.prefix}.$name';
|
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forType(T);
|
||||||
|
|
||||||
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
|
|
||||||
|
|
||||||
String encode(T value) => _codec.encode(value);
|
String encode(T value) => _codec.encode(value);
|
||||||
|
|
||||||
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
|
T decode(String raw) => _codec.decode(raw);
|
||||||
|
|
||||||
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class _MetadataCodec<T extends Object> {
|
sealed class _MetadataCodec<T extends Object> {
|
||||||
const _MetadataCodec();
|
const _MetadataCodec();
|
||||||
|
|
||||||
String encode(T value);
|
String encode(T value);
|
||||||
T? decode(String raw);
|
T decode(String raw);
|
||||||
|
|
||||||
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
||||||
int: _PrimitiveCodec.integer,
|
int: _PrimitiveCodec.integer,
|
||||||
@@ -146,12 +109,10 @@ sealed class _MetadataCodec<T extends Object> {
|
|||||||
DateTime: _DateTimeCodec(),
|
DateTime: _DateTimeCodec(),
|
||||||
};
|
};
|
||||||
|
|
||||||
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
|
static _MetadataCodec<T> forType<T extends Object>(Type runtimeType) {
|
||||||
final codec = _primitives[sample.runtimeType];
|
final codec = _primitives[runtimeType];
|
||||||
if (codec == null) {
|
if (codec == null) {
|
||||||
throw StateError(
|
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the MetadataKey.');
|
||||||
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return codec as _MetadataCodec<T>;
|
return codec as _MetadataCodec<T>;
|
||||||
}
|
}
|
||||||
@@ -166,7 +127,7 @@ final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
|
|||||||
String encode(T value) => value.name;
|
String encode(T value) => value.name;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
|
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
||||||
@@ -176,7 +137,7 @@ final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
|||||||
String encode(DateTime value) => value.toIso8601String();
|
String encode(DateTime value) => value.toIso8601String();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
DateTime decode(String raw) => DateTime.parse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
||||||
@@ -193,29 +154,26 @@ final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<K, V>? decode(String raw) {
|
Map<K, V> decode(String raw) {
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(raw);
|
final decoded = jsonDecode(raw);
|
||||||
if (decoded is! Map) {
|
if (decoded is! Map) {
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
final result = <K, V>{};
|
final result = <K, V>{};
|
||||||
for (final entry in decoded.entries) {
|
for (final entry in decoded.entries) {
|
||||||
final rawKey = entry.key;
|
final rawKey = entry.key;
|
||||||
final rawValue = entry.value;
|
final rawValue = entry.value;
|
||||||
if (rawKey is! String || rawValue is! String) {
|
if (rawKey is! String || rawValue is! String) {
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
final k = _keyCodec.decode(rawKey);
|
final k = _keyCodec.decode(rawKey);
|
||||||
final v = _valueCodec.decode(rawValue);
|
final v = _valueCodec.decode(rawValue);
|
||||||
if (k == null || v == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
result[k] = v;
|
result[k] = v;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} on FormatException {
|
} on FormatException {
|
||||||
return null;
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,32 +187,29 @@ final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
|||||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<T>? decode(String raw) {
|
List<T> decode(String raw) {
|
||||||
try {
|
try {
|
||||||
final decoded = jsonDecode(raw);
|
final decoded = jsonDecode(raw);
|
||||||
if (decoded is! List) {
|
if (decoded is! List) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
final result = <T>[];
|
final result = <T>[];
|
||||||
for (final item in decoded) {
|
for (final item in decoded) {
|
||||||
if (item is! String) {
|
if (item is! String) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
final element = _elementCodec.decode(item);
|
final element = _elementCodec.decode(item);
|
||||||
if (element == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
result.add(element);
|
result.add(element);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} on FormatException {
|
} on FormatException {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
||||||
final T? Function(String) _parse;
|
final T Function(String) _parse;
|
||||||
|
|
||||||
const _PrimitiveCodec._(this._parse);
|
const _PrimitiveCodec._(this._parse);
|
||||||
|
|
||||||
@@ -262,12 +217,12 @@ final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
|||||||
String encode(T value) => value.toString();
|
String encode(T value) => value.toString();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
T? decode(String raw) => _parse(raw);
|
T decode(String raw) => _parse(raw);
|
||||||
|
|
||||||
static const integer = _PrimitiveCodec<int>._(int.tryParse);
|
static const integer = _PrimitiveCodec<int>._(int.parse);
|
||||||
static const real = _PrimitiveCodec<double>._(double.tryParse);
|
static const real = _PrimitiveCodec<double>._(double.parse);
|
||||||
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
|
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
|
||||||
static const string = _PrimitiveCodec<String>._(_identity);
|
static const string = _PrimitiveCodec<String>._(_identity);
|
||||||
|
|
||||||
static String? _identity(String s) => s;
|
static String _identity(String s) => s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ class AssetService {
|
|||||||
final RemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
|
|
||||||
const AssetService({
|
const AssetService({required this._remoteAssetRepository, required this._localAssetRepository});
|
||||||
required RemoteAssetRepository remoteAssetRepository,
|
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
|
||||||
}) : _remoteAssetRepository = remoteAssetRepository,
|
|
||||||
_localAssetRepository = localAssetRepository;
|
|
||||||
|
|
||||||
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||||
|
|||||||
@@ -61,11 +61,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|||||||
|
|
||||||
bool _isCleanedUp = false;
|
bool _isCleanedUp = false;
|
||||||
|
|
||||||
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
|
BackgroundWorkerBgService({required this._drift, required this._driftLogger})
|
||||||
: _drift = drift,
|
: _backgroundHostApi = BackgroundWorkerBgHostApi() {
|
||||||
_driftLogger = driftLogger,
|
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(_drift))]);
|
||||||
_backgroundHostApi = BackgroundWorkerBgHostApi() {
|
|
||||||
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
|
|
||||||
BackgroundWorkerFlutterApi.setUp(this);
|
BackgroundWorkerFlutterApi.setUp(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,18 +21,13 @@ class HashService {
|
|||||||
final _log = Logger('HashService');
|
final _log = Logger('HashService');
|
||||||
|
|
||||||
HashService({
|
HashService({
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
required this._localAlbumRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required this._localAssetRepository,
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required this._trashedLocalAssetRepository,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required this._nativeSyncApi,
|
||||||
bool Function()? cancelChecker,
|
this._cancelChecker,
|
||||||
int? batchSize,
|
int? batchSize,
|
||||||
}) : _localAlbumRepository = localAlbumRepository,
|
}) : _batchSize = batchSize ?? kBatchHashFileLimit;
|
||||||
_localAssetRepository = localAssetRepository,
|
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
|
||||||
_cancelChecker = cancelChecker,
|
|
||||||
_nativeSyncApi = nativeSyncApi,
|
|
||||||
_batchSize = batchSize ?? kBatchHashFileLimit;
|
|
||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -23,29 +23,24 @@ class LocalSyncService {
|
|||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final NativeSyncApi _nativeSyncApi;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final StorageRepository _storageRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final Logger _log = Logger("DeviceSyncService");
|
final Logger _log = Logger("DeviceSyncService");
|
||||||
|
|
||||||
LocalSyncService({
|
LocalSyncService({
|
||||||
required DriftLocalAlbumRepository localAlbumRepository,
|
required this._localAlbumRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required this._localAssetRepository,
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required this._nativeSyncApi,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required this._trashedLocalAssetRepository,
|
||||||
required StorageRepository storageRepository,
|
required this._assetMediaRepository,
|
||||||
required NativeSyncApi nativeSyncApi,
|
required this._permissionRepository,
|
||||||
}) : _localAlbumRepository = localAlbumRepository,
|
});
|
||||||
_localAssetRepository = localAssetRepository,
|
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
|
||||||
_localFilesManager = localFilesManager,
|
|
||||||
_storageRepository = storageRepository,
|
|
||||||
_nativeSyncApi = nativeSyncApi;
|
|
||||||
|
|
||||||
Future<void> sync({bool full = false}) async {
|
Future<void> sync({bool full = false}) async {
|
||||||
final Stopwatch stopwatch = Stopwatch()..start();
|
final Stopwatch stopwatch = Stopwatch()..start();
|
||||||
try {
|
try {
|
||||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||||
final hasPermission = await _localFilesManager.hasManageMediaPermission();
|
final hasPermission = await _permissionRepository.hasManageMediaPermission();
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
await _syncTrashedAssets();
|
await _syncTrashedAssets();
|
||||||
} else {
|
} else {
|
||||||
@@ -373,7 +368,7 @@ class LocalSyncService {
|
|||||||
|
|
||||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||||
if (assetsToRestore.isNotEmpty) {
|
if (assetsToRestore.isNotEmpty) {
|
||||||
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore);
|
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||||
} else {
|
} else {
|
||||||
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
||||||
@@ -381,15 +376,15 @@ class LocalSyncService {
|
|||||||
|
|
||||||
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
||||||
if (localAssetsToTrash.isNotEmpty) {
|
if (localAssetsToTrash.isNotEmpty) {
|
||||||
final mediaUrls = await Future.wait(
|
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||||
localAssetsToTrash.values
|
_log.info("Moving to trash ${localIds.join(", ")} assets");
|
||||||
.expand((e) => e)
|
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
if (movedIds.isNotEmpty) {
|
||||||
);
|
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||||
_log.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
)..removeWhere((_, assets) => assets.isEmpty);
|
||||||
if (result) {
|
|
||||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
|
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class LogService {
|
|||||||
}) async {
|
}) async {
|
||||||
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
||||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||||
final level = instance._metadataRepository.systemConfig.logLevel;
|
final level = instance._metadataRepository.appConfig.logLevel;
|
||||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ typedef MapQuery = ({MapMarkerSource markerSource});
|
|||||||
class MapFactory {
|
class MapFactory {
|
||||||
final DriftMapRepository _mapRepository;
|
final DriftMapRepository _mapRepository;
|
||||||
|
|
||||||
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
|
const MapFactory({required this._mapRepository});
|
||||||
|
|
||||||
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
|
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
|
||||||
MapService(_mapRepository.remote(ownerIds, options));
|
MapService(_mapRepository.remote(ownerIds, options));
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ final AppSetting = SettingsService(storeService: StoreService.I);
|
|||||||
class SettingsService {
|
class SettingsService {
|
||||||
final StoreService _storeService;
|
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);
|
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/semver.dart';
|
import 'package:immich_mobile/utils/semver.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -34,31 +34,23 @@ class SyncStreamService {
|
|||||||
final SyncStreamRepository _syncStreamRepository;
|
final SyncStreamRepository _syncStreamRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||||
final LocalFilesManagerRepository _localFilesManager;
|
final AssetMediaRepository _assetMediaRepository;
|
||||||
final StorageRepository _storageRepository;
|
final IPermissionRepository _permissionRepository;
|
||||||
final SyncMigrationRepository _syncMigrationRepository;
|
final SyncMigrationRepository _syncMigrationRepository;
|
||||||
final ApiService _api;
|
final ApiService _api;
|
||||||
final bool Function()? _cancelChecker;
|
final bool Function()? _cancelChecker;
|
||||||
|
|
||||||
SyncStreamService({
|
SyncStreamService({
|
||||||
required SyncApiRepository syncApiRepository,
|
required this._syncApiRepository,
|
||||||
required SyncStreamRepository syncStreamRepository,
|
required this._syncStreamRepository,
|
||||||
required DriftLocalAssetRepository localAssetRepository,
|
required this._localAssetRepository,
|
||||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
required this._trashedLocalAssetRepository,
|
||||||
required LocalFilesManagerRepository localFilesManager,
|
required this._assetMediaRepository,
|
||||||
required StorageRepository storageRepository,
|
required this._permissionRepository,
|
||||||
required SyncMigrationRepository syncMigrationRepository,
|
required this._syncMigrationRepository,
|
||||||
required ApiService api,
|
required this._api,
|
||||||
bool Function()? cancelChecker,
|
this._cancelChecker,
|
||||||
}) : _syncApiRepository = syncApiRepository,
|
});
|
||||||
_syncStreamRepository = syncStreamRepository,
|
|
||||||
_localAssetRepository = localAssetRepository,
|
|
||||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
|
||||||
_localFilesManager = localFilesManager,
|
|
||||||
_storageRepository = storageRepository,
|
|
||||||
_syncMigrationRepository = syncMigrationRepository,
|
|
||||||
_api = api,
|
|
||||||
_cancelChecker = cancelChecker;
|
|
||||||
|
|
||||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||||
|
|
||||||
@@ -500,22 +492,22 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
||||||
final mediaUrls = await Future.wait(
|
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||||
localAssetsToTrash.values
|
_logger.info("Moving to trash ${localIds.join(", ")} assets");
|
||||||
.expand((e) => e)
|
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||||
.map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
|
if (movedIds.isNotEmpty) {
|
||||||
);
|
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||||
_logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
|
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||||
final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
|
)..removeWhere((_, assets) => assets.isEmpty);
|
||||||
if (result) {
|
|
||||||
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
|
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _applyRemoteRestoreToLocal() async {
|
Future<void> _applyRemoteRestoreToLocal() async {
|
||||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||||
if (assetsToRestore.isNotEmpty) {
|
if (assetsToRestore.isNotEmpty) {
|
||||||
final restoredIds = await _localFilesManager.restoreAssetsFromTrash(assetsToRestore);
|
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||||
} else {
|
} else {
|
||||||
_logger.info("No remote assets found for restoration");
|
_logger.info("No remote assets found for restoration");
|
||||||
@@ -523,7 +515,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
||||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||||
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -533,7 +525,7 @@ class SyncStreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
||||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||||
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ class TimelineFactory {
|
|||||||
final DriftTimelineRepository _timelineRepository;
|
final DriftTimelineRepository _timelineRepository;
|
||||||
final MetadataRepository _metadataRepository;
|
final MetadataRepository _metadataRepository;
|
||||||
|
|
||||||
const TimelineFactory({
|
const TimelineFactory({required this._timelineRepository, required this._metadataRepository});
|
||||||
required DriftTimelineRepository timelineRepository,
|
|
||||||
required MetadataRepository metadataRepository,
|
|
||||||
}) : _timelineRepository = timelineRepository,
|
|
||||||
_metadataRepository = metadataRepository;
|
|
||||||
|
|
||||||
GroupAssetsBy get groupBy {
|
GroupAssetsBy get groupBy {
|
||||||
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
|
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
|
||||||
@@ -108,12 +104,7 @@ class TimelineService {
|
|||||||
TimelineService(TimelineQuery query)
|
TimelineService(TimelineQuery query)
|
||||||
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
|
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
|
||||||
|
|
||||||
TimelineService._({
|
TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) {
|
||||||
required TimelineAssetSource assetSource,
|
|
||||||
required TimelineBucketSource bucketSource,
|
|
||||||
required this.origin,
|
|
||||||
}) : _assetSource = assetSource,
|
|
||||||
_bucketSource = bucketSource {
|
|
||||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||||
_mutex.run(() async {
|
_mutex.run(() async {
|
||||||
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ class UserService {
|
|||||||
final UserApiRepository _userApiRepository;
|
final UserApiRepository _userApiRepository;
|
||||||
final StoreService _storeService;
|
final StoreService _storeService;
|
||||||
|
|
||||||
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
|
UserService({required this._userApiRepository, required this._storeService});
|
||||||
: _userApiRepository = userApiRepository,
|
|
||||||
_storeService = storeService;
|
|
||||||
|
|
||||||
UserDto getMyUser() {
|
UserDto getMyUser() {
|
||||||
return _storeService.get(StoreKey.currentUser);
|
return _storeService.get(StoreKey.currentUser);
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/app_config.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/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/entities/metadata.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
class MetadataRepository extends DriftDatabaseRepository {
|
class MetadataRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
final Map<MetadataKey, Object> _cache = {};
|
|
||||||
|
|
||||||
MetadataRepository._(this._db) : super(_db);
|
MetadataRepository._(this._db) : super(_db);
|
||||||
|
|
||||||
@@ -25,153 +23,50 @@ class MetadataRepository extends DriftDatabaseRepository {
|
|||||||
AppConfig _appConfig = const .new();
|
AppConfig _appConfig = const .new();
|
||||||
AppConfig get appConfig => _appConfig;
|
AppConfig get appConfig => _appConfig;
|
||||||
|
|
||||||
SystemConfig _systemConfig = const .new();
|
|
||||||
SystemConfig get systemConfig => _systemConfig;
|
|
||||||
|
|
||||||
static Future<MetadataRepository> ensureInitialized(Drift db) async {
|
static Future<MetadataRepository> ensureInitialized(Drift db) async {
|
||||||
if (_instance == null) {
|
if (_instance == null) {
|
||||||
final instance = MetadataRepository._(db);
|
final instance = MetadataRepository._(db);
|
||||||
await instance._hydrate();
|
await instance.refresh();
|
||||||
_instance = instance;
|
_instance = instance;
|
||||||
}
|
}
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> refresh() async {
|
Future<void> refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get());
|
||||||
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 {
|
Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
|
||||||
if (_read(key) == value) {
|
if (value == _appConfig.read(key)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _db
|
if (value == defaultConfig.read(key)) {
|
||||||
.into(_db.metadataEntity)
|
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.name))).go();
|
||||||
.insertOnConflictUpdate(
|
} else {
|
||||||
MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
await _db
|
||||||
);
|
.into(_db.metadataEntity)
|
||||||
_updateCache(key, value);
|
.insertOnConflictUpdate(
|
||||||
}
|
MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||||
|
);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_appConfig = _appConfig.write(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCache<T extends Object>(MetadataKey<T> key, T value) {
|
Stream<AppConfig> watchConfig() => _db.select(_db.metadataEntity).watch().map((rows) {
|
||||||
if (_cache[key] == value) {
|
_applyOverrides(rows);
|
||||||
return;
|
return _appConfig;
|
||||||
}
|
});
|
||||||
_cache[key] = value;
|
|
||||||
key.domain.rebuild(this);
|
void _applyOverrides(List<MetadataEntityData> rows) {
|
||||||
}
|
_appConfig = AppConfig.fromEntries(
|
||||||
}
|
rows.fold({}, (overrides, row) {
|
||||||
|
final metadataKey = MetadataKey.values.firstWhereOrNull((key) => key.name == row.key);
|
||||||
extension<T extends Object> on MetadataDomain<T> {
|
if (metadataKey == null) {
|
||||||
T config(MetadataRepository repo) => switch (this) {
|
return overrides;
|
||||||
.appConfig => repo._appConfig as T,
|
}
|
||||||
.systemConfig => repo._systemConfig as T,
|
|
||||||
};
|
return {...overrides, metadataKey: metadataKey.decode(row.value)};
|
||||||
|
}),
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
_searchController = TextEditingController();
|
_searchController = TextEditingController();
|
||||||
_searchFocusNode = FocusNode();
|
_searchFocusNode = FocusNode();
|
||||||
|
|
||||||
_enableSyncUploadAlbum.value = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
_enableSyncUploadAlbum.value = ref.read(appConfigProvider).backup.syncAlbums;
|
||||||
ref.read(backupAlbumProvider.notifier).getAll();
|
ref.read(backupAlbumProvider.notifier).getAll();
|
||||||
|
|
||||||
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
_initialTotalAssetCount = ref.read(driftBackupProvider.select((p) => p.totalCount));
|
||||||
@@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableSyncUploadAlbum = ref.read(metadataProvider).appConfig.backup.syncAlbums;
|
final enableSyncUploadAlbum = ref.read(appConfigProvider).backup.syncAlbums;
|
||||||
final selectedAlbums = ref
|
final selectedAlbums = ref
|
||||||
.read(backupAlbumProvider)
|
.read(backupAlbumProvider)
|
||||||
.where((a) => a.backupSelection == BackupSelection.selected)
|
.where((a) => a.backupSelection == BackupSelection.selected)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
bool hasPopped = false;
|
bool hasPopped = false;
|
||||||
final previousBackup = ref.read(metadataProvider).appConfig.backup;
|
final previousBackup = ref.read(appConfigProvider).backup;
|
||||||
final previousCellularForVideos = previousBackup.useCellularForVideos;
|
final previousCellularForVideos = previousBackup.useCellularForVideos;
|
||||||
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
|
final previousCellularForPhotos = previousBackup.useCellularForPhotos;
|
||||||
return PopScope(
|
return PopScope(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class HeaderSettingsPage extends HookConsumerWidget {
|
|||||||
final headers = useState<List<SettingsHeader>>([]);
|
final headers = useState<List<SettingsHeader>>([]);
|
||||||
final setInitialHeaders = useState(false);
|
final setInitialHeaders = useState(false);
|
||||||
|
|
||||||
final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders;
|
final storedHeaders = ref.read(metadataProvider).appConfig.network.customHeaders;
|
||||||
if (!setInitialHeaders.value) {
|
if (!setInitialHeaders.value) {
|
||||||
storedHeaders.forEach((k, v) {
|
storedHeaders.forEach((k, v) {
|
||||||
final header = SettingsHeader();
|
final header = SettingsHeader();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/locales.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/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
@@ -36,7 +36,7 @@ class BootstrapErrorWidget extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext _) {
|
Widget build(BuildContext _) {
|
||||||
final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset;
|
final immichTheme = defaultConfig.theme.primaryColor.themeOfPreset;
|
||||||
|
|
||||||
return EasyLocalization(
|
return EasyLocalization(
|
||||||
supportedLocales: locales.values.toList(),
|
supportedLocales: locales.values.toList(),
|
||||||
|
|||||||
+19
@@ -654,6 +654,25 @@ class NativeSyncApi {
|
|||||||
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
|
return (pigeonVar_replyValue! as Map<Object?, Object?>).cast<String, List<PlatformAsset>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> restoreFromTrashById(String mediaId, int type) async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[mediaId, type]);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: false,
|
||||||
|
);
|
||||||
|
return pigeonVar_replyValue! as bool;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
|
Future<List<CloudIdResult>> getCloudIdForAssetIds(List<String> assetIds) async {
|
||||||
final pigeonVar_channelName =
|
final pigeonVar_channelName =
|
||||||
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
|
'dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds$pigeonVar_messageChannelSuffix';
|
||||||
|
|||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
|
||||||
|
// See also: https://pub.dev/packages/pigeon
|
||||||
|
// ignore_for_file: unused_import, unused_shown_name
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data' show Float64List, Int32List, Int64List;
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||||
|
|
||||||
|
Object? _extractReplyValueOrThrow(List<Object?>? replyList, String channelName, {required bool isNullValid}) {
|
||||||
|
if (replyList == null) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'channel-error',
|
||||||
|
message: 'Unable to establish connection on channel: "$channelName".',
|
||||||
|
);
|
||||||
|
} else if (replyList.length > 1) {
|
||||||
|
throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]);
|
||||||
|
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: 'null-error',
|
||||||
|
message: 'Host platform returned null value for non-null return value.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return replyList.firstOrNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PigeonCodec extends StandardMessageCodec {
|
||||||
|
const _PigeonCodec();
|
||||||
|
@override
|
||||||
|
void writeValue(WriteBuffer buffer, Object? value) {
|
||||||
|
if (value is int) {
|
||||||
|
buffer.putUint8(4);
|
||||||
|
buffer.putInt64(value);
|
||||||
|
} else {
|
||||||
|
super.writeValue(buffer, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||||
|
switch (type) {
|
||||||
|
default:
|
||||||
|
return super.readValueOfType(type, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionApi {
|
||||||
|
/// Constructor for [PermissionApi]. The [binaryMessenger] named argument is
|
||||||
|
/// available for dependency injection. If it is left null, the default
|
||||||
|
/// BinaryMessenger will be used which routes to the host platform.
|
||||||
|
PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||||
|
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||||
|
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||||
|
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||||
|
|
||||||
|
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||||
|
|
||||||
|
final String pigeonVar_messageChannelSuffix;
|
||||||
|
|
||||||
|
Future<bool> hasManageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.hasManageMediaPermission$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: false,
|
||||||
|
);
|
||||||
|
return pigeonVar_replyValue! as bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> requestManageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.requestManageMediaPermission$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: false,
|
||||||
|
);
|
||||||
|
return pigeonVar_replyValue! as bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> manageMediaPermission() async {
|
||||||
|
final pigeonVar_channelName =
|
||||||
|
'dev.flutter.pigeon.immich_mobile.PermissionApi.manageMediaPermission$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
|
||||||
|
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||||
|
pigeonVar_replyList,
|
||||||
|
pigeonVar_channelName,
|
||||||
|
isNullValid: false,
|
||||||
|
);
|
||||||
|
return pigeonVar_replyValue! as bool;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.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:
|
/// This delete action has the following behavior:
|
||||||
/// - Prompt to delete the asset locally
|
/// - Prompt to delete the asset locally
|
||||||
@@ -39,6 +40,8 @@ class DeleteLocalActionButton extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.invalidate(localAlbumProvider);
|
||||||
|
|
||||||
final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
final successMessage = 'delete_local_action_prompt'.t(context: context, args: {'count': result.count.toString()});
|
||||||
|
|
||||||
if (context.mounted) {
|
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';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class _SharePreparingDialog extends StatelessWidget {
|
class _SharePreparingDialog extends StatelessWidget {
|
||||||
const _SharePreparingDialog();
|
final ValueNotifier<double?> progress;
|
||||||
|
|
||||||
|
const _SharePreparingDialog({required this.progress});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -22,8 +24,24 @@ class _SharePreparingDialog extends StatelessWidget {
|
|||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const CircularProgressIndicator(),
|
Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()),
|
||||||
Container(margin: const EdgeInsets.only(top: 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>();
|
final cancelCompleter = Completer<void>();
|
||||||
const preparingDialog = _SharePreparingDialog();
|
final progress = ValueNotifier<double?>(null);
|
||||||
|
final preparingDialog = _SharePreparingDialog(progress: progress);
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext buildContext) {
|
builder: (BuildContext buildContext) {
|
||||||
ref.read(actionProvider.notifier).shareAssets(source, context, cancelCompleter: cancelCompleter).then((
|
ref
|
||||||
ActionResult result,
|
.read(actionProvider.notifier)
|
||||||
) {
|
.shareAssets(
|
||||||
if (cancelCompleter.isCompleted || !context.mounted) {
|
source,
|
||||||
return;
|
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) {
|
if (!result.success) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'scaffold_body_error_occurred'.t(context: context),
|
msg: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
buildContext.pop();
|
buildContext.pop();
|
||||||
});
|
});
|
||||||
|
|
||||||
// show a loading spinner with a "Preparing" message
|
// Show download progress with a "Preparing" message
|
||||||
return preparingDialog;
|
return preparingDialog;
|
||||||
},
|
},
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -77,6 +102,7 @@ class ShareActionButton extends ConsumerWidget {
|
|||||||
if (!cancelCompleter.isCompleted) {
|
if (!cancelCompleter.isCompleted) {
|
||||||
cancelCompleter.complete();
|
cancelCompleter.complete();
|
||||||
}
|
}
|
||||||
|
progress.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -296,16 +296,12 @@ class _ThumbnailRenderBox extends RenderBox {
|
|||||||
bool isRepaintBoundary = true;
|
bool isRepaintBoundary = true;
|
||||||
|
|
||||||
_ThumbnailRenderBox({
|
_ThumbnailRenderBox({
|
||||||
required ui.Image? image,
|
required this._image,
|
||||||
required ui.Image? previousImage,
|
required this._previousImage,
|
||||||
required double fadeValue,
|
required this._fadeValue,
|
||||||
required BoxFit fit,
|
required this._fit,
|
||||||
required Gradient placeholderGradient,
|
required this._placeholderGradient,
|
||||||
}) : _image = image,
|
});
|
||||||
_previousImage = previousImage,
|
|
||||||
_fadeValue = fadeValue,
|
|
||||||
_fit = fit,
|
|
||||||
_placeholderGradient = placeholderGradient;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
|||||||
@@ -62,14 +62,11 @@ class RenderFixedRow extends RenderBox
|
|||||||
RenderBoxContainerDefaultsMixin<RenderBox, _RowParentData> {
|
RenderBoxContainerDefaultsMixin<RenderBox, _RowParentData> {
|
||||||
RenderFixedRow({
|
RenderFixedRow({
|
||||||
List<RenderBox>? children,
|
List<RenderBox>? children,
|
||||||
required double height,
|
required this._height,
|
||||||
required List<double> widths,
|
required this._widths,
|
||||||
required double spacing,
|
required this._spacing,
|
||||||
required TextDirection textDirection,
|
required this._textDirection,
|
||||||
}) : _height = height,
|
}) {
|
||||||
_widths = widths,
|
|
||||||
_spacing = spacing,
|
|
||||||
_textDirection = textDirection {
|
|
||||||
addAll(children);
|
addAll(children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -578,9 +578,7 @@ class _SlideFadeTransition extends StatelessWidget {
|
|||||||
final Animation<double> _animation;
|
final Animation<double> _animation;
|
||||||
final Widget _child;
|
final Widget _child;
|
||||||
|
|
||||||
const _SlideFadeTransition({required Animation<double> animation, required Widget child})
|
const _SlideFadeTransition({required this._animation, required this._child});
|
||||||
: _animation = animation,
|
|
||||||
_child = child;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final grid = CustomScrollView(
|
final grid = CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
physics: _scrollPhysics,
|
physics: _scrollPhysics,
|
||||||
cacheExtent: maxHeight * 2,
|
scrollCacheExtent: .pixels(maxHeight * 2),
|
||||||
slivers: [
|
slivers: [
|
||||||
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
|
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
|
||||||
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
||||||
@@ -503,7 +503,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
|
class _SliverSegmentedList extends SliverMultiBoxAdaptorWidget {
|
||||||
final List<Segment> _segments;
|
final List<Segment> _segments;
|
||||||
|
|
||||||
const _SliverSegmentedList({required List<Segment> segments, required super.delegate}) : _segments = segments;
|
const _SliverSegmentedList({required this._segments, required super.delegate});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
|
_RenderSliverTimelineBoxAdaptor createRenderObject(BuildContext context) =>
|
||||||
@@ -527,8 +527,7 @@ class _RenderSliverTimelineBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
|||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
_RenderSliverTimelineBoxAdaptor({required super.childManager, required List<Segment> segments})
|
_RenderSliverTimelineBoxAdaptor({required super.childManager, required this._segments});
|
||||||
: _segments = segments;
|
|
||||||
|
|
||||||
int getMinChildIndexForScrollOffset(double offset) =>
|
int getMinChildIndexForScrollOffset(double offset) =>
|
||||||
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
|
_segments.findByOffset(offset)?.getMinChildIndexForScrollOffset(offset) ?? 0;
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
await _apiService.updateHeaders();
|
await _apiService.updateHeaders();
|
||||||
|
|
||||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||||
final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders;
|
final headerMap = _ref.read(metadataProvider).appConfig.network.customHeaders;
|
||||||
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
|
final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap);
|
||||||
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
|
await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders);
|
||||||
|
|
||||||
@@ -187,11 +187,11 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String? getSavedWifiName() {
|
String? getSavedWifiName() {
|
||||||
return _ref.read(metadataProvider).systemConfig.network.preferredWifiName;
|
return _ref.read(metadataProvider).appConfig.network.preferredWifiName;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getSavedLocalEndpoint() {
|
String? getSavedLocalEndpoint() {
|
||||||
return _ref.read(metadataProvider).systemConfig.network.localEndpoint;
|
return _ref.read(metadataProvider).appConfig.network.localEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current server endpoint (with /api) URL from the store
|
/// Returns the current server endpoint (with /api) URL from the store
|
||||||
|
|||||||
@@ -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.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/asset.provider.dart' show assetExifProvider;
|
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/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/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.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/action.service.dart';
|
||||||
import 'package:immich_mobile/services/download.service.dart';
|
import 'package:immich_mobile/services/download.service.dart';
|
||||||
import 'package:immich_mobile/services/foreground_upload.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:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
@@ -463,11 +465,17 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
ActionSource source,
|
ActionSource source,
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
Completer<void>? cancelCompleter,
|
Completer<void>? cancelCompleter,
|
||||||
|
void Function(double progress)? onAssetDownloadProgress,
|
||||||
}) async {
|
}) async {
|
||||||
final ids = _getAssets(source).toList(growable: false);
|
final ids = _getAssets(source).toList(growable: false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _service.shareAssets(ids, context, cancelCompleter: cancelCompleter);
|
await _service.shareAssets(
|
||||||
|
ids,
|
||||||
|
context,
|
||||||
|
cancelCompleter: cancelCompleter,
|
||||||
|
onAssetDownloadProgress: onAssetDownloadProgress,
|
||||||
|
);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to share assets', 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');
|
return ActionResult(count: ids.length, success: false, error: 'Expected single asset for applying edits');
|
||||||
}
|
}
|
||||||
|
|
||||||
final completer = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV1", (dynamic data) {
|
Future<void> editReady;
|
||||||
final eventAsset = SyncAssetV1.fromJson(data["asset"]);
|
if (ref.read(serverInfoProvider).serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)) {
|
||||||
return eventAsset?.id == ids.first;
|
editReady = ref.read(websocketProvider.notifier).waitForEvent("AssetEditReadyV2", (dynamic data) {
|
||||||
}, const Duration(seconds: 10));
|
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 {
|
try {
|
||||||
await _service.applyEdits(ids.first, edits);
|
await _service.applyEdits(ids.first, edits);
|
||||||
await completer;
|
await editReady;
|
||||||
return const ActionResult(count: 1, success: true);
|
return const ActionResult(count: 1, success: true);
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Failed to apply edits to assets', error, stack);
|
_logger.severe('Failed to apply edits to assets', error, stack);
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/app_config.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';
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
|
|
||||||
final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance);
|
final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance);
|
||||||
|
|
||||||
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
||||||
final repo = ref.watch(metadataProvider);
|
final repo = ref.watch(metadataProvider);
|
||||||
final subscription = repo.watchAppConfig().listen((event) => ref.state = event);
|
final subscription = repo.watchConfig().listen((event) => ref.state = event);
|
||||||
ref.onDispose(subscription.cancel);
|
ref.onDispose(subscription.cancel);
|
||||||
return repo.appConfig;
|
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;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
|||||||
import 'package:immich_mobile/platform/background_worker_api.g.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/platform/background_worker_lock_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
|
||||||
import 'package:immich_mobile/platform/local_image_api.g.dart';
|
import 'package:immich_mobile/platform/local_image_api.g.dart';
|
||||||
|
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/network_api.g.dart';
|
import 'package:immich_mobile/platform/network_api.g.dart';
|
||||||
|
import 'package:immich_mobile/platform/permission_api.g.dart';
|
||||||
import 'package:immich_mobile/platform/remote_image_api.g.dart';
|
import 'package:immich_mobile/platform/remote_image_api.g.dart';
|
||||||
|
|
||||||
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||||
@@ -16,6 +17,8 @@ final backgroundWorkerLockServiceProvider = Provider<BackgroundWorkerLockService
|
|||||||
|
|
||||||
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||||
|
|
||||||
|
final permissionApiProvider = Provider<PermissionApi>((_) => PermissionApi());
|
||||||
|
|
||||||
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
||||||
|
|
||||||
final localImageApi = LocalImageApi();
|
final localImageApi = LocalImageApi();
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
|
|
||||||
final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));
|
final syncMigrationRepositoryProvider = Provider((ref) => SyncMigrationRepository(ref.watch(driftProvider)));
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ final syncStreamServiceProvider = Provider(
|
|||||||
syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
|
syncStreamRepository: ref.watch(syncStreamRepositoryProvider),
|
||||||
localAssetRepository: ref.watch(localAssetRepository),
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
|
||||||
storageRepository: ref.watch(storageRepositoryProvider),
|
permissionRepository: ref.watch(permissionRepositoryProvider),
|
||||||
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
|
syncMigrationRepository: ref.watch(syncMigrationRepositoryProvider),
|
||||||
api: ref.watch(apiServiceProvider),
|
api: ref.watch(apiServiceProvider),
|
||||||
cancelChecker: ref.watch(cancellationProvider),
|
cancelChecker: ref.watch(cancellationProvider),
|
||||||
@@ -39,8 +39,8 @@ final localSyncServiceProvider = Provider(
|
|||||||
localAlbumRepository: ref.watch(localAlbumRepository),
|
localAlbumRepository: ref.watch(localAlbumRepository),
|
||||||
localAssetRepository: ref.watch(localAssetRepository),
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
trashedLocalAssetRepository: ref.watch(trashedLocalAssetRepository),
|
||||||
localFilesManager: ref.watch(localFilesManagerRepositoryProvider),
|
assetMediaRepository: ref.watch(assetMediaRepositoryProvider),
|
||||||
storageRepository: ref.watch(storageRepositoryProvider),
|
permissionRepository: ref.watch(permissionRepositoryProvider),
|
||||||
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
nativeSyncApi: ref.watch(nativeSyncApiProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
import 'package:device_info_plus/device_info_plus.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_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/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.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:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
|
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(nativeSyncApiProvider)));
|
||||||
|
|
||||||
class AssetMediaRepository {
|
class AssetMediaRepository {
|
||||||
final AssetApiRepository _assetApiRepository;
|
final NativeSyncApi _nativeSyncApi;
|
||||||
static final Logger _log = Logger("AssetMediaRepository");
|
static final Logger _log = Logger("AssetMediaRepository");
|
||||||
|
|
||||||
const AssetMediaRepository(this._assetApiRepository);
|
const AssetMediaRepository(this._nativeSyncApi);
|
||||||
|
|
||||||
Future<bool> _androidSupportsTrash() async {
|
Future<bool> _androidSupportsTrash() async {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
@@ -45,6 +48,27 @@ class AssetMediaRepository {
|
|||||||
return PhotoManager.editor.deleteWithIds(ids);
|
return PhotoManager.editor.deleteWithIds(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _restoreFromTrashById(String mediaId, int type) async {
|
||||||
|
try {
|
||||||
|
return await _nativeSyncApi.restoreFromTrashById(mediaId, type);
|
||||||
|
} catch (e, s) {
|
||||||
|
_log.warning('Error restore file from trash by Id', e, s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
|
||||||
|
final restoredIds = <String>[];
|
||||||
|
for (final asset in assets) {
|
||||||
|
_log.info("Restoring from trash, localId: ${asset.id}, checksum: ${asset.checksum}");
|
||||||
|
final result = await _restoreFromTrashById(asset.id, asset.type.index);
|
||||||
|
if (result) {
|
||||||
|
restoredIds.add(asset.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restoredIds;
|
||||||
|
}
|
||||||
|
|
||||||
Future<AssetEntity?> get(String id) async {
|
Future<AssetEntity?> get(String id) async {
|
||||||
final entity = await AssetEntity.fromId(id);
|
final entity = await AssetEntity.fromId(id);
|
||||||
return entity;
|
return entity;
|
||||||
@@ -81,10 +105,29 @@ class AssetMediaRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this more efficient
|
Future<int> shareAssets(
|
||||||
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) async {
|
List<BaseAsset> assets,
|
||||||
|
BuildContext context, {
|
||||||
|
Completer<void>? cancelCompleter,
|
||||||
|
void Function(double progress)? onAssetDownloadProgress,
|
||||||
|
}) async {
|
||||||
final downloadedXFiles = <XFile>[];
|
final downloadedXFiles = <XFile>[];
|
||||||
final tempFiles = <File>[];
|
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) {
|
for (var asset in assets) {
|
||||||
if (cancelCompleter != null && cancelCompleter.isCompleted) {
|
if (cancelCompleter != null && cancelCompleter.isCompleted) {
|
||||||
@@ -101,6 +144,8 @@ class AssetMediaRepository {
|
|||||||
if (localId != null && !asset.isEdited) {
|
if (localId != null && !asset.isEdited) {
|
||||||
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
|
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
|
||||||
downloadedXFiles.add(XFile(f!.path));
|
downloadedXFiles.add(XFile(f!.path));
|
||||||
|
processedAssets++;
|
||||||
|
updateProgress();
|
||||||
if (CurrentPlatform.isIOS) {
|
if (CurrentPlatform.isIOS) {
|
||||||
tempFiles.add(f);
|
tempFiles.add(f);
|
||||||
}
|
}
|
||||||
@@ -108,22 +153,50 @@ class AssetMediaRepository {
|
|||||||
final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId;
|
final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId;
|
||||||
if (remoteId == null) {
|
if (remoteId == null) {
|
||||||
_log.warning("Asset has no remote ID for sharing: $asset");
|
_log.warning("Asset has no remote ID for sharing: $asset");
|
||||||
|
processedAssets++;
|
||||||
|
updateProgress();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final taskId = 'share-$remoteId-${DateTime.now().microsecondsSinceEpoch}';
|
||||||
final name = asset.name;
|
final sanitizedFilename = asset.name.replaceAll(RegExp(r'[\\/]'), '_');
|
||||||
final tempFile = await File('${tempDir.path}/$name').create();
|
final task = DownloadTask(
|
||||||
final res = await _assetApiRepository.downloadAsset(remoteId, edited: true);
|
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) {
|
if (cancelCompleter != null && cancelCompleter.isCompleted) {
|
||||||
_log.severe("Download for $name failed", res.toLoggerString());
|
await _cleanupTempFiles(tempFiles);
|
||||||
continue;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await tempFile.writeAsBytes(res.bodyBytes);
|
if (statusUpdate.status == TaskStatus.complete) {
|
||||||
downloadedXFiles.add(XFile(tempFile.path));
|
final filePath = await task.filePath();
|
||||||
tempFiles.add(tempFile);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +1,38 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
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/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/infrastructure/repositories/sync_stream.repository.dart';
|
||||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.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/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
|
|
||||||
final authRepositoryProvider = Provider<AuthRepository>(
|
final authRepositoryProvider = Provider<AuthRepository>(
|
||||||
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)),
|
(ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
class AuthRepository {
|
class AuthRepository {
|
||||||
final Drift _drift;
|
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 {
|
Future<void> clearLocalData() async {
|
||||||
await SyncStreamRepository(_drift).reset();
|
await SyncStreamRepository(_drift).reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getEndpointSwitchingFeature() {
|
bool getEndpointSwitchingFeature() {
|
||||||
return _metadata.systemConfig.network.autoEndpointSwitching;
|
return _config.network.autoEndpointSwitching;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getPreferredWifiName() {
|
String? getPreferredWifiName() {
|
||||||
return _metadata.systemConfig.network.preferredWifiName;
|
return _config.network.preferredWifiName;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? getLocalEndpoint() {
|
String? getLocalEndpoint() {
|
||||||
return _metadata.systemConfig.network.localEndpoint;
|
return _config.network.localEndpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AuxilaryEndpoint> getExternalEndpointList() {
|
List<AuxilaryEndpoint> getExternalEndpointList() {
|
||||||
return _metadata.systemConfig.network.externalEndpointList
|
return _config.network.externalEndpointList.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList();
|
||||||
.map((url) => AuxilaryEndpoint(url: url, status: .valid))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/services/local_files_manager.service.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
final localFilesManagerRepositoryProvider = Provider(
|
|
||||||
(ref) => LocalFilesManagerRepository(ref.watch(localFileManagerServiceProvider)),
|
|
||||||
);
|
|
||||||
|
|
||||||
class LocalFilesManagerRepository {
|
|
||||||
LocalFilesManagerRepository(this._service);
|
|
||||||
|
|
||||||
final Logger _logger = Logger('LocalFilesManagerRepo');
|
|
||||||
final LocalFilesManagerService _service;
|
|
||||||
|
|
||||||
Future<bool> moveToTrash(List<String> mediaUrls) async {
|
|
||||||
return await _service.moveToTrash(mediaUrls);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
|
||||||
return await _service.restoreFromTrash(fileName, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> requestManageMediaPermission() async {
|
|
||||||
return await _service.requestManageMediaPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasManageMediaPermission() async {
|
|
||||||
return await _service.hasManageMediaPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> manageMediaPermission() async {
|
|
||||||
return await _service.manageMediaPermission();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<String>> restoreAssetsFromTrash(Iterable<LocalAsset> assets) async {
|
|
||||||
final restoredIds = <String>[];
|
|
||||||
for (final asset in assets) {
|
|
||||||
_logger.info("Restoring from trash, localId: ${asset.id}, remoteId: ${asset.checksum}");
|
|
||||||
try {
|
|
||||||
final result = await _service.restoreFromTrashById(asset.id, asset.type.index);
|
|
||||||
if (result) {
|
|
||||||
restoredIds.add(asset.id);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
_logger.warning("Restoring failure: $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return restoredIds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/platform/permission_api.g.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
final permissionRepositoryProvider = Provider((_) {
|
final permissionRepositoryProvider = Provider((ref) {
|
||||||
return const PermissionRepository();
|
return PermissionRepository(ref.watch(permissionApiProvider));
|
||||||
});
|
});
|
||||||
|
|
||||||
class PermissionRepository implements IPermissionRepository {
|
class PermissionRepository implements IPermissionRepository {
|
||||||
const PermissionRepository();
|
final PermissionApi _permissionApi;
|
||||||
|
|
||||||
|
const PermissionRepository(this._permissionApi);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> hasLocationWhenInUsePermission() {
|
Future<bool> hasLocationWhenInUsePermission() {
|
||||||
@@ -34,6 +38,21 @@ class PermissionRepository implements IPermissionRepository {
|
|||||||
Future<bool> openSettings() {
|
Future<bool> openSettings() {
|
||||||
return openAppSettings();
|
return openAppSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> hasManageMediaPermission() {
|
||||||
|
return _permissionApi.hasManageMediaPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> requestManageMediaPermission() {
|
||||||
|
return _permissionApi.requestManageMediaPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> manageMediaPermission() {
|
||||||
|
return _permissionApi.manageMediaPermission();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract interface class IPermissionRepository {
|
abstract interface class IPermissionRepository {
|
||||||
@@ -42,4 +61,7 @@ abstract interface class IPermissionRepository {
|
|||||||
Future<bool> hasLocationAlwaysPermission();
|
Future<bool> hasLocationAlwaysPermission();
|
||||||
Future<bool> requestLocationAlwaysPermission();
|
Future<bool> requestLocationAlwaysPermission();
|
||||||
Future<bool> openSettings();
|
Future<bool> openSettings();
|
||||||
|
Future<bool> hasManageMediaPermission();
|
||||||
|
Future<bool> requestManageMediaPermission();
|
||||||
|
Future<bool> manageMediaPermission();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,8 +269,18 @@ class ActionService {
|
|||||||
await _assetApiRepository.unStack(stackIds);
|
await _assetApiRepository.unStack(stackIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) {
|
Future<int> shareAssets(
|
||||||
return _assetMediaRepository.shareAssets(assets, context, cancelCompleter: cancelCompleter);
|
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) {
|
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
|
||||||
|
|||||||
@@ -177,9 +177,9 @@ class ApiService {
|
|||||||
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
if (serverEndpoint != null && serverEndpoint.isNotEmpty) {
|
||||||
urls.add(serverEndpoint);
|
urls.add(serverEndpoint);
|
||||||
}
|
}
|
||||||
final network = MetadataRepository.instance.systemConfig.network;
|
final network = MetadataRepository.instance.appConfig.network;
|
||||||
final localEndpoint = network.localEndpoint;
|
final localEndpoint = network.localEndpoint;
|
||||||
if (localEndpoint != null) {
|
if (localEndpoint.isNotEmpty) {
|
||||||
urls.add(localEndpoint);
|
urls.add(localEndpoint);
|
||||||
}
|
}
|
||||||
for (final url in network.externalEndpointList) {
|
for (final url in network.externalEndpointList) {
|
||||||
@@ -191,7 +191,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, String> getRequestHeaders() {
|
static Map<String, String> getRequestHeaders() {
|
||||||
return MetadataRepository.instance.systemConfig.network.customHeaders;
|
return MetadataRepository.instance.appConfig.network.customHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiClient get apiClient => _apiClient;
|
ApiClient get apiClient => _apiClient;
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
final localFileManagerServiceProvider = Provider<LocalFilesManagerService>((ref) => const LocalFilesManagerService());
|
|
||||||
|
|
||||||
class LocalFilesManagerService {
|
|
||||||
const LocalFilesManagerService();
|
|
||||||
|
|
||||||
static final Logger _logger = Logger('LocalFilesManager');
|
|
||||||
static const MethodChannel _channel = MethodChannel('file_trash');
|
|
||||||
|
|
||||||
Future<bool> moveToTrash(List<String> mediaUrls) async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('moveToTrash', {'mediaUrls': mediaUrls});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error moving file to trash', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> restoreFromTrash(String fileName, int type) async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('restoreFromTrash', {'fileName': fileName, 'type': type});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error restore file from trash', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> restoreFromTrashById(String mediaId, int type) async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('restoreFromTrash', {'mediaId': mediaId, 'type': type});
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error restore file from trash by Id', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> requestManageMediaPermission() async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('requestManageMediaPermission');
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error requesting manage media permission', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> hasManageMediaPermission() async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('hasManageMediaPermission');
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error requesting manage media permission state', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> manageMediaPermission() async {
|
|
||||||
try {
|
|
||||||
return await _channel.invokeMethod('manageMediaPermission');
|
|
||||||
} catch (e, s) {
|
|
||||||
_logger.warning('Error requesting manage media permission settings', e, s);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/colors.dart';
|
import 'package:immich_mobile/constants/colors.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.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/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
@@ -136,15 +138,11 @@ Future<void> _migrateTo26(Drift drift) async {
|
|||||||
|
|
||||||
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
|
Future<void> _migrateAlbumSortMode(_StoreMigrator migrator) async {
|
||||||
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id);
|
final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id);
|
||||||
if (raw == null) {
|
final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw);
|
||||||
|
if (mode == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mode = AlbumSortMode.values.firstWhere(
|
|
||||||
(e) => e.storeIndex == raw,
|
|
||||||
orElse: () => MetadataKey.albumSortMode.defaultValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode);
|
migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +206,11 @@ class _StoreMigrator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue;
|
final enumValue = values.elementAtOrNull(index);
|
||||||
|
if (enumValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cache[newKey] = enumValue;
|
_cache[newKey] = enumValue;
|
||||||
_migratedStoreIds.add(legacyKey.id);
|
_migratedStoreIds.add(legacyKey.id);
|
||||||
}
|
}
|
||||||
@@ -223,7 +225,11 @@ class _StoreMigrator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue);
|
final enumValue = values.firstWhereOrNull((e) => e.name == name);
|
||||||
|
if (enumValue == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_cache[newKey] = enumValue;
|
_cache[newKey] = enumValue;
|
||||||
_migratedStoreIds.add(legacyKey.id);
|
_migratedStoreIds.add(legacyKey.id);
|
||||||
}
|
}
|
||||||
@@ -267,9 +273,12 @@ class _StoreMigrator {
|
|||||||
Future<void> complete() async {
|
Future<void> complete() async {
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
for (final entry in _cache.entries) {
|
for (final entry in _cache.entries) {
|
||||||
|
if (entry.value == defaultConfig.read(entry.key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
batch.insert(
|
batch.insert(
|
||||||
_db.metadataEntity,
|
_db.metadataEntity,
|
||||||
MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))),
|
MetadataEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))),
|
||||||
mode: InsertMode.insertOrReplace,
|
mode: InsertMode.insertOrReplace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/oauth.provider.dart';
|
import 'package:immich_mobile/providers/oauth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/provider_utils.dart';
|
import 'package:immich_mobile/utils/provider_utils.dart';
|
||||||
import 'package:immich_mobile/utils/url_helper.dart';
|
import 'package:immich_mobile/utils/url_helper.dart';
|
||||||
@@ -193,7 +193,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getManageMediaPermission() async {
|
getManageMediaPermission() async {
|
||||||
final hasPermission = await ref.read(localFilesManagerRepositoryProvider).hasManageMediaPermission();
|
final hasPermission = await ref.read(permissionRepositoryProvider).hasManageMediaPermission();
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -224,7 +224,7 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission();
|
unawaited(ref.read(permissionRepositoryProvider).requestManageMediaPermission());
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
|
|
||||||
PhotoViewHeroAttributes? get heroAttributes => widget.heroAttributes;
|
PhotoViewHeroAttributes? get heroAttributes => widget.heroAttributes;
|
||||||
|
|
||||||
late ScaleBoundaries cachedScaleBoundaries = widget.scaleBoundaries;
|
|
||||||
|
|
||||||
void handleScaleAnimation() {
|
void handleScaleAnimation() {
|
||||||
scale = _scaleAnimation!.value;
|
scale = _scaleAnimation!.value;
|
||||||
}
|
}
|
||||||
@@ -303,7 +301,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
controller.scaleAnimationBuilder(_animateControllerScale);
|
controller.scaleAnimationBuilder(_animateControllerScale);
|
||||||
controller.rotationAnimationBuilder(_animateControllerRotation);
|
controller.rotationAnimationBuilder(_animateControllerRotation);
|
||||||
|
|
||||||
cachedScaleBoundaries = widget.scaleBoundaries;
|
_updateScaleBoundaries();
|
||||||
|
|
||||||
_scaleAnimationController = AnimationController(vsync: this)
|
_scaleAnimationController = AnimationController(vsync: this)
|
||||||
..addListener(handleScaleAnimation)
|
..addListener(handleScaleAnimation)
|
||||||
@@ -334,14 +332,29 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
widget.onTapDown?.call(context, details, controller.value);
|
widget.onTapDown?.call(context, details, controller.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _updateScaleBoundaries() {
|
||||||
Widget build(BuildContext context) {
|
final prev = controller.scaleBoundaries;
|
||||||
// Check if we need a recalc on the scale
|
if (prev == widget.scaleBoundaries) {
|
||||||
if (widget.scaleBoundaries != cachedScaleBoundaries) {
|
return;
|
||||||
markNeedsScaleRecalc = true;
|
|
||||||
cachedScaleBoundaries = widget.scaleBoundaries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prev != null && controller.scale != null && prev.initialScale > 0) {
|
||||||
|
final ratio = widget.scaleBoundaries.initialScale / prev.initialScale;
|
||||||
|
controller.setScaleInvisibly(controller.scale! * ratio);
|
||||||
|
} else {
|
||||||
|
markNeedsScaleRecalc = true;
|
||||||
|
}
|
||||||
|
controller.scaleBoundaries = widget.scaleBoundaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PhotoViewCore oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_updateScaleBoundaries();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return StreamBuilder(
|
return StreamBuilder(
|
||||||
stream: controller.outputStateStream,
|
stream: controller.outputStateStream,
|
||||||
initialData: controller.prevValue,
|
initialData: controller.prevValue,
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
|||||||
_lastStack = null;
|
_lastStack = null;
|
||||||
|
|
||||||
_didLoadSynchronously = synchronousCall;
|
_didLoadSynchronously = synchronousCall;
|
||||||
widget.controller.scaleBoundaries = scaleBoundaries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB);
|
synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||||||
import 'package:immich_mobile/providers/infrastructure/metadata.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/platform.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
|
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
@@ -31,7 +31,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid);
|
||||||
final isManageMediaSupported = useState(false);
|
final isManageMediaSupported = useState(false);
|
||||||
final manageMediaAndroidPermission = useState(false);
|
final manageMediaAndroidPermission = useState(false);
|
||||||
final levelId = useState<int>(ref.read(systemConfigProvider).logLevel.index);
|
final levelId = useState<int>(ref.read(appConfigProvider).logLevel.index);
|
||||||
final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote);
|
final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote);
|
||||||
useValueChanged(
|
useValueChanged(
|
||||||
preferRemote.value,
|
preferRemote.value,
|
||||||
@@ -57,9 +57,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
() async {
|
() async {
|
||||||
isManageMediaSupported.value = await checkAndroidVersion();
|
isManageMediaSupported.value = await checkAndroidVersion();
|
||||||
if (isManageMediaSupported.value) {
|
if (isManageMediaSupported.value) {
|
||||||
manageMediaAndroidPermission.value = await ref
|
manageMediaAndroidPermission.value = await ref.read(permissionRepositoryProvider).hasManageMediaPermission();
|
||||||
.read(localFilesManagerRepositoryProvider)
|
|
||||||
.hasManageMediaPermission();
|
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
return null;
|
return null;
|
||||||
@@ -82,7 +80,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
subtitle: "advanced_settings_sync_remote_deletions_subtitle".tr(),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (value) {
|
if (value) {
|
||||||
final result = await ref.read(localFilesManagerRepositoryProvider).requestManageMediaPermission();
|
final result = await ref.read(permissionRepositoryProvider).requestManageMediaPermission();
|
||||||
manageLocalMediaAndroid.value = result;
|
manageLocalMediaAndroid.value = result;
|
||||||
manageMediaAndroidPermission.value = result;
|
manageMediaAndroidPermission.value = result;
|
||||||
}
|
}
|
||||||
@@ -96,7 +94,7 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
? const Color.fromARGB(255, 243, 188, 106)
|
? const Color.fromARGB(255, 243, 188, 106)
|
||||||
: null,
|
: null,
|
||||||
onActionTap: () async {
|
onActionTap: () async {
|
||||||
final result = await ref.read(localFilesManagerRepositoryProvider).manageMediaPermission();
|
final result = await ref.read(permissionRepositoryProvider).manageMediaPermission();
|
||||||
manageMediaAndroidPermission.value = result;
|
manageMediaAndroidPermission.value = result;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/services/localization.service.dart';
|
import 'package:immich_mobile/services/localization.service.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
|
|
||||||
class LanguageSettings extends HookConsumerWidget {
|
class LanguageSettings extends HookConsumerWidget {
|
||||||
@@ -84,7 +85,7 @@ class LanguageSettings extends HookConsumerWidget {
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
itemCount: filteredLocaleEntries.value.length,
|
itemCount: filteredLocaleEntries.value.length,
|
||||||
itemExtent: 64.0,
|
itemExtent: 64.0,
|
||||||
cacheExtent: 100,
|
scrollCacheExtent: const .pixels(100),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final countryName = filteredLocaleEntries.value[index].key;
|
final countryName = filteredLocaleEntries.value[index].key;
|
||||||
final localeValue = filteredLocaleEntries.value[index].value;
|
final localeValue = filteredLocaleEntries.value[index].value;
|
||||||
|
|||||||
@@ -36,10 +36,6 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleReorder(int oldIndex, int newIndex) {
|
handleReorder(int oldIndex, int newIndex) {
|
||||||
if (oldIndex < newIndex) {
|
|
||||||
newIndex -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
final entry = entries.value.removeAt(oldIndex);
|
final entry = entries.value.removeAt(oldIndex);
|
||||||
entries.value.insert(newIndex, entry);
|
entries.value.insert(newIndex, entry);
|
||||||
entries.value = [...entries.value];
|
entries.value = [...entries.value];
|
||||||
@@ -68,7 +64,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
final urls = ref.read(metadataProvider).systemConfig.network.externalEndpointList;
|
final urls = ref.read(appConfigProvider).network.externalEndpointList;
|
||||||
|
|
||||||
if (urls.isEmpty) {
|
if (urls.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
@@ -113,7 +109,7 @@ class ExternalNetworkPreference extends HookConsumerWidget {
|
|||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: entries.value.length,
|
itemCount: entries.value.length,
|
||||||
onReorder: handleReorder,
|
onReorderItem: handleReorder,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return EndpointInput(
|
return EndpointInput(
|
||||||
key: Key(index.toString()),
|
key: Key(index.toString()),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class NetworkingSettings extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final currentEndpoint = getServerUrl();
|
final currentEndpoint = getServerUrl();
|
||||||
final featureEnabled = useState(ref.read(systemConfigProvider).network.autoEndpointSwitching);
|
final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching);
|
||||||
useValueChanged<bool, void>(featureEnabled.value, (_, __) {
|
useValueChanged<bool, void>(featureEnabled.value, (_, __) {
|
||||||
ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value);
|
ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -78,6 +78,15 @@ alias = "migration"
|
|||||||
description = "Generate database migrations"
|
description = "Generate database migrations"
|
||||||
run = "dart run drift_dev make-migrations"
|
run = "dart run drift_dev make-migrations"
|
||||||
|
|
||||||
|
[tasks.install]
|
||||||
|
alias = "install"
|
||||||
|
description = "Install flutter dependencies"
|
||||||
|
run = "flutter pub get"
|
||||||
|
|
||||||
|
[tasks.start]
|
||||||
|
alias = "start"
|
||||||
|
description = "Start flutter app"
|
||||||
|
run = "flutter run"
|
||||||
|
|
||||||
# Internal tasks
|
# Internal tasks
|
||||||
[tasks."i18n:loader"]
|
[tasks."i18n:loader"]
|
||||||
|
|||||||
Generated
+6
@@ -206,6 +206,7 @@ Class | Method | HTTP request | Description
|
|||||||
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
|
*PeopleApi* | [**updatePerson**](doc//PeopleApi.md#updateperson) | **PUT** /people/{id} | Update person
|
||||||
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
|
*PluginsApi* | [**getPlugin**](doc//PluginsApi.md#getplugin) | **GET** /plugins/{id} | Retrieve a plugin
|
||||||
*PluginsApi* | [**searchPluginMethods**](doc//PluginsApi.md#searchpluginmethods) | **GET** /plugins/methods | Retrieve plugin methods
|
*PluginsApi* | [**searchPluginMethods**](doc//PluginsApi.md#searchpluginmethods) | **GET** /plugins/methods | Retrieve plugin methods
|
||||||
|
*PluginsApi* | [**searchPluginTemplates**](doc//PluginsApi.md#searchplugintemplates) | **GET** /plugins/templates | Retrieve workflow templates
|
||||||
*PluginsApi* | [**searchPlugins**](doc//PluginsApi.md#searchplugins) | **GET** /plugins | List all plugins
|
*PluginsApi* | [**searchPlugins**](doc//PluginsApi.md#searchplugins) | **GET** /plugins | List all plugins
|
||||||
*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue
|
*QueuesApi* | [**emptyQueue**](doc//QueuesApi.md#emptyqueue) | **DELETE** /queues/{name}/jobs | Empty a queue
|
||||||
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
|
*QueuesApi* | [**getQueue**](doc//QueuesApi.md#getqueue) | **GET** /queues/{name} | Retrieve a queue
|
||||||
@@ -491,6 +492,8 @@ Class | Method | HTTP request | Description
|
|||||||
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
- [PlacesResponseDto](doc//PlacesResponseDto.md)
|
||||||
- [PluginMethodResponseDto](doc//PluginMethodResponseDto.md)
|
- [PluginMethodResponseDto](doc//PluginMethodResponseDto.md)
|
||||||
- [PluginResponseDto](doc//PluginResponseDto.md)
|
- [PluginResponseDto](doc//PluginResponseDto.md)
|
||||||
|
- [PluginTemplateResponseDto](doc//PluginTemplateResponseDto.md)
|
||||||
|
- [PluginTemplateStepResponseDto](doc//PluginTemplateStepResponseDto.md)
|
||||||
- [PurchaseResponse](doc//PurchaseResponse.md)
|
- [PurchaseResponse](doc//PurchaseResponse.md)
|
||||||
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
- [PurchaseUpdate](doc//PurchaseUpdate.md)
|
||||||
- [QueueCommand](doc//QueueCommand.md)
|
- [QueueCommand](doc//QueueCommand.md)
|
||||||
@@ -510,6 +513,9 @@ Class | Method | HTTP request | Description
|
|||||||
- [RatingsUpdate](doc//RatingsUpdate.md)
|
- [RatingsUpdate](doc//RatingsUpdate.md)
|
||||||
- [ReactionLevel](doc//ReactionLevel.md)
|
- [ReactionLevel](doc//ReactionLevel.md)
|
||||||
- [ReactionType](doc//ReactionType.md)
|
- [ReactionType](doc//ReactionType.md)
|
||||||
|
- [ReleaseChannel](doc//ReleaseChannel.md)
|
||||||
|
- [ReleaseEventV1](doc//ReleaseEventV1.md)
|
||||||
|
- [ReleaseType](doc//ReleaseType.md)
|
||||||
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
|
||||||
- [RotateParameters](doc//RotateParameters.md)
|
- [RotateParameters](doc//RotateParameters.md)
|
||||||
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
- [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md)
|
||||||
|
|||||||
Generated
+5
@@ -237,6 +237,8 @@ part 'model/pin_code_setup_dto.dart';
|
|||||||
part 'model/places_response_dto.dart';
|
part 'model/places_response_dto.dart';
|
||||||
part 'model/plugin_method_response_dto.dart';
|
part 'model/plugin_method_response_dto.dart';
|
||||||
part 'model/plugin_response_dto.dart';
|
part 'model/plugin_response_dto.dart';
|
||||||
|
part 'model/plugin_template_response_dto.dart';
|
||||||
|
part 'model/plugin_template_step_response_dto.dart';
|
||||||
part 'model/purchase_response.dart';
|
part 'model/purchase_response.dart';
|
||||||
part 'model/purchase_update.dart';
|
part 'model/purchase_update.dart';
|
||||||
part 'model/queue_command.dart';
|
part 'model/queue_command.dart';
|
||||||
@@ -256,6 +258,9 @@ part 'model/ratings_response.dart';
|
|||||||
part 'model/ratings_update.dart';
|
part 'model/ratings_update.dart';
|
||||||
part 'model/reaction_level.dart';
|
part 'model/reaction_level.dart';
|
||||||
part 'model/reaction_type.dart';
|
part 'model/reaction_type.dart';
|
||||||
|
part 'model/release_channel.dart';
|
||||||
|
part 'model/release_event_v1.dart';
|
||||||
|
part 'model/release_type.dart';
|
||||||
part 'model/reverse_geocoding_state_response_dto.dart';
|
part 'model/reverse_geocoding_state_response_dto.dart';
|
||||||
part 'model/rotate_parameters.dart';
|
part 'model/rotate_parameters.dart';
|
||||||
part 'model/search_album_response_dto.dart';
|
part 'model/search_album_response_dto.dart';
|
||||||
|
|||||||
Generated
+51
@@ -204,6 +204,57 @@ class PluginsApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve workflow templates
|
||||||
|
///
|
||||||
|
/// Retrieve workflow templates provided by installed plugins
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
Future<Response> searchPluginTemplatesWithHttpInfo() async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/plugins/templates';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve workflow templates
|
||||||
|
///
|
||||||
|
/// Retrieve workflow templates provided by installed plugins
|
||||||
|
Future<List<PluginTemplateResponseDto>?> searchPluginTemplates() async {
|
||||||
|
final response = await searchPluginTemplatesWithHttpInfo();
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<PluginTemplateResponseDto>') as List)
|
||||||
|
.cast<PluginTemplateResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// List all plugins
|
/// List all plugins
|
||||||
///
|
///
|
||||||
/// Retrieve a list of plugins available to the authenticated user.
|
/// Retrieve a list of plugins available to the authenticated user.
|
||||||
|
|||||||
Generated
+10
@@ -520,6 +520,10 @@ class ApiClient {
|
|||||||
return PluginMethodResponseDto.fromJson(value);
|
return PluginMethodResponseDto.fromJson(value);
|
||||||
case 'PluginResponseDto':
|
case 'PluginResponseDto':
|
||||||
return PluginResponseDto.fromJson(value);
|
return PluginResponseDto.fromJson(value);
|
||||||
|
case 'PluginTemplateResponseDto':
|
||||||
|
return PluginTemplateResponseDto.fromJson(value);
|
||||||
|
case 'PluginTemplateStepResponseDto':
|
||||||
|
return PluginTemplateStepResponseDto.fromJson(value);
|
||||||
case 'PurchaseResponse':
|
case 'PurchaseResponse':
|
||||||
return PurchaseResponse.fromJson(value);
|
return PurchaseResponse.fromJson(value);
|
||||||
case 'PurchaseUpdate':
|
case 'PurchaseUpdate':
|
||||||
@@ -558,6 +562,12 @@ class ApiClient {
|
|||||||
return ReactionLevelTypeTransformer().decode(value);
|
return ReactionLevelTypeTransformer().decode(value);
|
||||||
case 'ReactionType':
|
case 'ReactionType':
|
||||||
return ReactionTypeTypeTransformer().decode(value);
|
return ReactionTypeTypeTransformer().decode(value);
|
||||||
|
case 'ReleaseChannel':
|
||||||
|
return ReleaseChannelTypeTransformer().decode(value);
|
||||||
|
case 'ReleaseEventV1':
|
||||||
|
return ReleaseEventV1.fromJson(value);
|
||||||
|
case 'ReleaseType':
|
||||||
|
return ReleaseTypeTypeTransformer().decode(value);
|
||||||
case 'ReverseGeocodingStateResponseDto':
|
case 'ReverseGeocodingStateResponseDto':
|
||||||
return ReverseGeocodingStateResponseDto.fromJson(value);
|
return ReverseGeocodingStateResponseDto.fromJson(value);
|
||||||
case 'RotateParameters':
|
case 'RotateParameters':
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user