mirror of
https://github.com/immich-app/immich.git
synced 2026-05-31 03:45:19 -04:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc2c01e473 | |||
| 0a3878cfb8 | |||
| 306df18432 | |||
| 2ce4d9fa61 | |||
| 65611bb860 | |||
| 14aff51da9 | |||
| c42cea5ca9 | |||
| da8505f61d | |||
| 58586483dc | |||
| a838167f11 | |||
| b189fc571c | |||
| 96923f6115 | |||
| 0d6cce4a5b | |||
| 55947cb227 | |||
| 8783180cf3 | |||
| 134c0d4dfb | |||
| aecf8ec88b | |||
| 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 | |||
| 7b9dab872b | |||
| 6413495fb8 | |||
| b414b3d32b | |||
| 20da7c4267 | |||
| 92b6778d2d | |||
| 5a61e589e8 | |||
| 85192bb110 | |||
| c7ae97fa2b | |||
| 8d02f3625d | |||
| a5a7380a26 | |||
| d9ce3d2046 | |||
| 815ff677fc |
@@ -8,6 +8,8 @@ log "Preparing Immich Web Frontend"
|
||||
log ""
|
||||
run_cmd pnpm --filter @immich/sdk install
|
||||
run_cmd pnpm --filter @immich/sdk build
|
||||
run_cmd pnpm --filter @immich/plugin-sdk install
|
||||
run_cmd pnpm --filter @immich/plugin-sdk build
|
||||
run_cmd pnpm --filter immich-web install
|
||||
|
||||
log "Starting Immich Web Frontend"
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -230,8 +230,12 @@ jobs:
|
||||
- name: Generate platform APIs
|
||||
run: mise //mobile:codegen:pigeon
|
||||
|
||||
- name: Resolve iOS Swift Packages
|
||||
working-directory: ./mobile
|
||||
run: flutter build ios --config-only --no-codesign
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||
uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
@@ -288,7 +292,6 @@ jobs:
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||
ENVIRONMENT: ${{ inputs.environment || 'development' }}
|
||||
BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
|
||||
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@26ccb332c67a45ca649de9faf60552ef1b8260d9 # v0.0.46
|
||||
uses: oasdiff/oasdiff-action/breaking@6147a58e5d1249a12f42fc864ab791d571a30015 # v0.0.47
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -13,3 +13,4 @@ jobs:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
secrets: inherit
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -72,10 +72,6 @@ jobs:
|
||||
run: flutter pub get
|
||||
working-directory: ./mobile/packages/ui
|
||||
|
||||
- name: Install dependencies for UI Showcase
|
||||
run: flutter pub get
|
||||
working-directory: ./mobile/packages/ui/showcase
|
||||
|
||||
- name: Generate translation files
|
||||
run: mise //mobile:codegen:translation
|
||||
|
||||
|
||||
+20
-13
@@ -30,25 +30,32 @@ jobs:
|
||||
filters: |
|
||||
i18n:
|
||||
- 'i18n/**'
|
||||
- 'mise.toml'
|
||||
web:
|
||||
- 'web/**'
|
||||
- 'i18n/**'
|
||||
- 'packages/sdk/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'mise.toml'
|
||||
server:
|
||||
- 'server/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'mise.toml'
|
||||
cli:
|
||||
- 'packages/cli/**'
|
||||
- 'packages/sdk/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'mise.toml'
|
||||
e2e:
|
||||
- 'e2e/**'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'mise.toml'
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
- 'mise.toml'
|
||||
machine-learning:
|
||||
- 'machine-learning/**'
|
||||
- 'mise.toml'
|
||||
.github:
|
||||
- '.github/**'
|
||||
force-filters: |
|
||||
@@ -76,7 +83,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -107,7 +114,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -138,7 +145,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -182,7 +189,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -220,7 +227,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -248,7 +255,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -298,7 +305,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -331,7 +338,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -550,7 +557,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -587,7 +594,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -618,7 +625,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -669,7 +676,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
@@ -727,7 +734,7 @@ jobs:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cf6e190bacde3d7bda59372a786b36ac7d01536a # use-mise-action-v2.0.1
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.token.outputs.token }}
|
||||
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
dev:
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise dev # or mise //:dev from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-down:
|
||||
docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-down # or mise //:dev-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-update:
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-update # or mise //:dev-update from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-scale:
|
||||
@trap 'make dev-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise dev-scale # or mise //:dev-scale from another directory\n\n" >&2 && exit 1
|
||||
|
||||
dev-docs:
|
||||
npm --prefix docs run start
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e # or mise //:e2e from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-dev:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.dev.yml up --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-dev # or mise //:e2e-dev from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-update:
|
||||
@trap 'make e2e-down' EXIT; COMPOSE_BAKE=true docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-update # or mise //:e2e-update from another directory\n\n" >&2 && exit 1
|
||||
|
||||
e2e-down:
|
||||
docker compose -f ./e2e/docker-compose.yml down --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise e2e-down # or mise //:e2e-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod:
|
||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise prod # or mise //:prod from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod-down:
|
||||
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-down # or mise //:prod-down from another directory\n\n" >&2 && exit 1
|
||||
|
||||
prod-scale:
|
||||
@trap 'make prod-down' EXIT; COMPOSE_BAKE=true docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||
@printf "This command has been removed. Please use:\n\n mise prod-scale # or mise //:prod-scale from another directory\n\n" >&2 && exit 1
|
||||
|
||||
.PHONY: open-api
|
||||
open-api:
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise open-api # or mise //:open-api from another directory\n\n" >&2 && exit 1
|
||||
|
||||
sql:
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n"\n\n >&2 && exit 1
|
||||
@printf "This command has been removed. Please use:\n\n mise sql # or mise //:sql from another directory\n\n" >&2 && exit 1
|
||||
|
||||
|
||||
renovate:
|
||||
@@ -52,16 +52,7 @@ renovate:
|
||||
MODULES = e2e server web cli sdk docs .github
|
||||
|
||||
test-e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml build
|
||||
pnpm --filter immich-e2e run test
|
||||
pnpm --filter immich-e2e run test:web
|
||||
@printf "This command has been removed. Please use:\n\n mise //e2e:test # or mise //e2e:test-web for web tests, respectively\n\n" >&2 && exit 1
|
||||
|
||||
clean:
|
||||
find . -name "node_modules" -type d -prune -exec rm -rf {} +
|
||||
find . -name "dist" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "build" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name "coverage" -type d -prune -exec rm -rf '{}' +
|
||||
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
|
||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
|
||||
@printf "This command has been removed. Please use:\n\n mise clean # or mise //:clean from another directory\n\n" >&2 && exit 1
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.11.6"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64"]
|
||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.macos-arm64"]
|
||||
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
version = "1.0.3"
|
||||
backend = "aqua:gruntwork-io/terragrunt"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.macos-arm64"]
|
||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.macos-x64"]
|
||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.windows-x64"]
|
||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
volumes:
|
||||
- ..:/usr/src/app
|
||||
# - ../../ui:/usr/src/ui
|
||||
- pnpm_cache:/buildcache/pnpm_cache
|
||||
- build_cache:/buildcache
|
||||
- server_node_modules:/usr/src/app/server/node_modules
|
||||
- web_node_modules:/usr/src/app/web/node_modules
|
||||
- github_node_modules:/usr/src/app/.github/node_modules
|
||||
@@ -45,11 +45,11 @@ services:
|
||||
target: dev
|
||||
command:
|
||||
- |
|
||||
pnpm install
|
||||
mise install
|
||||
touch /tmp/init-complete
|
||||
exec tail -f /dev/null
|
||||
volumes:
|
||||
- pnpm_store_server:/buildcache/pnpm-store
|
||||
- build_cache:/buildcache
|
||||
restart: 'no'
|
||||
healthcheck:
|
||||
test: ['CMD', 'test', '-f', '/tmp/init-complete']
|
||||
@@ -73,7 +73,6 @@ services:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}/photos:/data
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- pnpm_store_server:/buildcache/pnpm-store
|
||||
- ../packages/plugin-core:/build/plugins/immich-plugin-core
|
||||
env_file:
|
||||
- .env
|
||||
@@ -122,8 +121,6 @@ services:
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 24678:24678
|
||||
volumes:
|
||||
- pnpm_store_web:/buildcache/pnpm-store
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
immich-init:
|
||||
@@ -203,9 +200,7 @@ volumes:
|
||||
model_cache:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
pnpm_cache:
|
||||
pnpm_store_server:
|
||||
pnpm_store_web:
|
||||
build_cache:
|
||||
server_node_modules:
|
||||
web_node_modules:
|
||||
github_node_modules:
|
||||
|
||||
@@ -26,6 +26,8 @@ For organizations seeking to resell Immich, we have established the following gu
|
||||
|
||||
When in doubt or if you have an edge case scenario, we encourage you to contact us directly via email to discuss the use of our trademark. We can provide clear guidance on what is acceptable and what is not. You can reach out at: questions@immich.app
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
|
||||
### How can I reset the admin password?
|
||||
@@ -36,6 +38,10 @@ The admin password can be reset by running the [reset-admin-password](/administr
|
||||
|
||||
You can see the list of all users by running [list-users](/administration/server-commands.md) Command on the Immich-server.
|
||||
|
||||
### How can I change my profile picture?
|
||||
|
||||
View a single photo, press the three dots in the top-right to show context menu, and select "Set as profile picture". In the pop-up, use your mouse scroll wheel to zoom in the picture until it completely fills the circle. Click and drag the picture to align it to your liking. Press "Save" to save your changes.
|
||||
|
||||
---
|
||||
|
||||
## Mobile App
|
||||
|
||||
@@ -5,7 +5,7 @@ After making any changes in the `server/src/schema`, a database migration need t
|
||||
1. Run the command
|
||||
|
||||
```bash
|
||||
pnpm run migrations:generate <migration-name>
|
||||
mise //server:migrations generate <migration-name>
|
||||
```
|
||||
|
||||
2. Check if the migration file makes sense.
|
||||
@@ -18,7 +18,7 @@ The server will automatically detect `*.ts` file changes and restart. Part of th
|
||||
If you need to undo the most recently applied migration—for example, when developing or testing on schema changes—run:
|
||||
|
||||
```bash
|
||||
pnpm run migrations:revert
|
||||
mise //server:migrations revert
|
||||
```
|
||||
|
||||
This command rolls back the latest migration and brings the database schema back to its previous state.
|
||||
|
||||
@@ -252,44 +252,33 @@ To connect the mobile app to your Dev Container:
|
||||
|
||||
The Dev Container supports multiple ways to run tests:
|
||||
|
||||
#### Using Mise Commands (Recommended)
|
||||
|
||||
```bash
|
||||
# Run tests for specific components
|
||||
mise run checklist # in `server/`, `web/`, `packages/cli`
|
||||
# Server
|
||||
mise //server:test # unit tests
|
||||
mise //server:test-medium # medium / integration tests
|
||||
|
||||
# Web
|
||||
mise //web:test # unit tests
|
||||
|
||||
# E2E
|
||||
mise //e2e:test # API tests
|
||||
mise //e2e:test-web # web UI tests (Playwright)
|
||||
|
||||
# Run all checks for a component
|
||||
mise //server:checklist
|
||||
mise //web:checklist
|
||||
```
|
||||
|
||||
#### Using PNPM Directly
|
||||
|
||||
```bash
|
||||
# Server tests
|
||||
cd /workspaces/immich/server
|
||||
pnpm test # Run all tests
|
||||
pnpm run test:medium # Medium tests (integration tests)
|
||||
pnpm run test:watch # Watch mode
|
||||
pnpm run test:cov # Coverage report
|
||||
|
||||
# Web tests
|
||||
cd /workspaces/immich/web
|
||||
pnpm test # Run all tests
|
||||
pnpm run test:watch # Watch mode
|
||||
|
||||
# E2E tests
|
||||
cd /workspaces/immich/e2e
|
||||
pnpm run test # Run API tests
|
||||
pnpm run test:web # Run web UI tests
|
||||
```
|
||||
|
||||
### Additional Make Commands
|
||||
### Additional Commands
|
||||
|
||||
```bash
|
||||
# API generation
|
||||
make open-api # Generate OpenAPI specs
|
||||
make open-api-typescript # Generate TypeScript SDK
|
||||
make open-api-dart # Generate Dart SDK
|
||||
mise //:open-api # Generate OpenAPI specs
|
||||
mise //:open-api-typescript # Generate TypeScript SDK
|
||||
mise //:open-api-dart # Generate Dart SDK
|
||||
|
||||
# Database
|
||||
mise sql # Sync database schema
|
||||
mise //server:sql # Sync database schema
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
@@ -8,34 +8,42 @@ When contributing code through a pull request, please check the following:
|
||||
|
||||
## Web Checks
|
||||
|
||||
- [ ] `pnpm run lint` (linting via ESLint)
|
||||
- [ ] `pnpm run format` (formatting via Prettier)
|
||||
- [ ] `pnpm run check:svelte` (Type checking via SvelteKit)
|
||||
- [ ] `pnpm run check:typescript` (check typescript)
|
||||
- [ ] `pnpm test` (unit tests)
|
||||
- [ ] `mise //web:lint` (linting via ESLint)
|
||||
- [ ] `mise //web:format` (formatting via Prettier)
|
||||
- [ ] `mise //web:check-svelte` (type checking via SvelteKit)
|
||||
- [ ] `mise //web:check-typescript` (type checking via `tsc`)
|
||||
- [ ] `mise //web:test` (unit tests)
|
||||
|
||||
:::tip AIO
|
||||
Run all web checks with `pnpm run check:all`
|
||||
Run all web checks with `mise //web:checklist`
|
||||
:::
|
||||
|
||||
:::tip Auto Fix
|
||||
Use `mise //web:lint-fix` and `mise //web:format-fix` to automatically correct some issues.
|
||||
:::
|
||||
|
||||
## Documentation
|
||||
|
||||
- [ ] `pnpm run format` (formatting via Prettier)
|
||||
- [ ] `mise //docs:format` (formatting via Prettier)
|
||||
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
|
||||
|
||||
:::tip Auto Fix
|
||||
Use `mise //docs:format-fix` to automatically fix formatting.
|
||||
:::
|
||||
|
||||
## Server Checks
|
||||
|
||||
- [ ] `pnpm run lint` (linting via ESLint)
|
||||
- [ ] `pnpm run format` (formatting via Prettier)
|
||||
- [ ] `pnpm run check` (Type checking via `tsc`)
|
||||
- [ ] `pnpm test` (unit tests)
|
||||
- [ ] `mise //server:lint` (linting via ESLint)
|
||||
- [ ] `mise //server:format` (formatting via Prettier)
|
||||
- [ ] `mise //server:check` (type checking via `tsc`)
|
||||
- [ ] `mise //server:test` (unit tests)
|
||||
|
||||
:::tip AIO
|
||||
Run all server checks with `pnpm run check:all`
|
||||
Run all server checks with `mise //server:checklist`
|
||||
:::
|
||||
|
||||
:::tip Auto Fix
|
||||
You can use `pnpm run __:fix` to potentially correct some issues automatically for `pnpm run format` and `lint`.
|
||||
Use `mise //server:lint-fix` and `mise //server:format-fix` to automatically correct some issues.
|
||||
:::
|
||||
|
||||
## Mobile Checklist
|
||||
@@ -53,6 +61,17 @@ Run all these commands at once with `mise //mobile:checklist`
|
||||
You can use `mise //mobile:lint-fix` to potentially correct some issues automatically for `mise //mobile:lint`.
|
||||
:::
|
||||
|
||||
## Machine Learning Checklist
|
||||
|
||||
- [ ] `mise //machine-learning:lint` (linting via ruff)
|
||||
- [ ] `mise //machine-learning:format` (formatting via ruff)
|
||||
- [ ] `mise //machine-learning:check` (type checking via mypy)
|
||||
- [ ] `mise //machine-learning:test` (unit tests via pytest)
|
||||
|
||||
:::tip AIO
|
||||
Run all machine learning checks with `mise //machine-learning:checklist`
|
||||
:::
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/api.md) for more details.
|
||||
|
||||
@@ -32,6 +32,10 @@ This environment includes the services below. Additional details are available i
|
||||
|
||||
All the services are packaged to run as with single Docker Compose command.
|
||||
|
||||
:::tip mise
|
||||
[mise](https://mise.jdx.dev) is used throughout the project to manage tool versions and run tasks. [Install mise](https://mise.jdx.dev/installing-mise.html), then from the repo root run `mise trust` and `mise install` to get all required tools. Tasks for each service can be run from the repo root using `mise //namespace:task` (e.g. `mise //server:lint`). To list all available tasks, run `mise tasks ls --all`.
|
||||
:::
|
||||
|
||||
### Server and web apps
|
||||
|
||||
1. Clone the project repo.
|
||||
@@ -56,22 +60,23 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
|
||||
|
||||
#### Connect web to a remote backend
|
||||
|
||||
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
||||
|
||||
1. Build the Immich SDK - `pnpm --filter @immich/sdk install && pnpm --filter @immich/sdk build`
|
||||
2. Enter the web directory - `cd web/`
|
||||
3. Install web dependencies - `pnpm i`
|
||||
4. Start the web development server
|
||||
If you only want to do web development connected to an existing, remote backend, run from the repo root:
|
||||
|
||||
```bash
|
||||
IMMICH_SERVER_URL=https://demo.immich.app/ pnpm run dev
|
||||
IMMICH_SERVER_URL=https://demo.immich.app/ mise //web:start
|
||||
```
|
||||
|
||||
This will install all dependencies (including the SDK) and start the dev server in one step. To connect to the hosted demo server specifically, use the shorthand:
|
||||
|
||||
```bash
|
||||
mise //web:start-demo
|
||||
```
|
||||
|
||||
If you're using PowerShell on Windows you may need to set the env var separately like so:
|
||||
|
||||
```powershell
|
||||
$env:IMMICH_SERVER_URL = "https://demo.immich.app/"
|
||||
pnpm run dev
|
||||
mise //web:start
|
||||
```
|
||||
|
||||
#### `@immich/ui`
|
||||
@@ -90,24 +95,38 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
||||
|
||||
#### Setup
|
||||
|
||||
1. [Install mise](https://mise.jdx.dev/installing-mise.html).
|
||||
2. Change to the immich (root) directory and trust the mise config with `mise trust`.
|
||||
3. Install tools with mise: `mise install`.
|
||||
4. Change to the `mobile/` directory.
|
||||
5. Run `flutter pub get` to install the dependencies.
|
||||
6. Run `make translation` to generate the translation file.
|
||||
7. Run `flutter run` to start the app.
|
||||
1. Run `mise //mobile:install` to install Flutter dependencies.
|
||||
2. Run `mise //mobile:translation` to generate the translation file.
|
||||
3. Change to the `mobile/` directory and run `flutter run` to start the app.
|
||||
|
||||
#### Translation
|
||||
|
||||
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then, from the `mobile/` directory, run
|
||||
To add a new translation text, enter the key-value pair in the `i18n/en.json` in the root of the immich project. Then run:
|
||||
|
||||
```bash
|
||||
make translation
|
||||
mise //mobile:translation
|
||||
```
|
||||
|
||||
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
|
||||
|
||||
#### UI components and widget previews
|
||||
|
||||
Shared design-system widgets (buttons, inputs, forms) live in the
|
||||
[`immich_ui` package](https://github.com/immich-app/immich/tree/main/mobile/packages/ui/)
|
||||
under `mobile/packages/ui/`. Components are defined in `lib/src/components/`
|
||||
and have matching previews in `lib/src/previews/`.
|
||||
|
||||
To inspect a component in isolation with a light/dark toggle and hot reload,
|
||||
launch [Flutter's Widget Previewer](https://docs.flutter.dev/tools/widget-previewer):
|
||||
|
||||
```bash
|
||||
cd mobile/packages/ui
|
||||
flutter widget-preview start
|
||||
```
|
||||
|
||||
In VS Code or Android Studio with the Flutter plugin, the previewer
|
||||
auto-starts when you open the **Flutter Widget Preview** tab in the sidebar.
|
||||
|
||||
## IDE setup
|
||||
|
||||
### Lint / format extensions
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
### Unit tests
|
||||
|
||||
Unit are run by calling `pnpm run test` from the `server/` directory.
|
||||
You need to run `pnpm install` (in `server/`) before _once_.
|
||||
Unit tests are run with `mise //server:test`.
|
||||
You need to run `mise //server:install` before _once_.
|
||||
|
||||
### End to end tests
|
||||
|
||||
@@ -17,8 +17,7 @@ make e2e
|
||||
|
||||
Before you can run the tests, you need to run the following commands _once_:
|
||||
|
||||
- `pnpm install`
|
||||
- `pnpm --filter @immich/sdk --filter @immich/cli build`
|
||||
- `mise //e2e:ci-setup` (installs e2e, SDK, and CLI dependencies)
|
||||
- `mise //:open-api`
|
||||
|
||||
Once the test environment is running, the e2e tests can be run via:
|
||||
|
||||
@@ -154,33 +154,33 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
|
||||
## Machine Learning
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__OCR__RECOGNITION` | Comma-separated list of (recognition) OCR model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__OCR__DETECTION` | Comma-separated list of (detection) OCR model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning |
|
||||
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spun up while inferencing. | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
|
||||
| `MACHINE_LEARNING_OPENVINO_PRECISION` | If set to FP16, uses half-precision floating-point operations for faster inference with reduced accuracy (one of [`FP16`, `FP32`], applies only to OpenVINO) | `FP32` | machine learning |
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `300` (`900` if using ROCm) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__OCR__RECOGNITION` | Comma-separated list of (recognition) OCR model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__OCR__DETECTION` | Comma-separated list of (detection) OCR model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning |
|
||||
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spun up while inferencing. | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
|
||||
| `MACHINE_LEARNING_OPENVINO_PRECISION` | If set to FP16, uses half-precision floating-point operations for faster inference with reduced accuracy (one of [`FP16`, `FP32`], applies only to OpenVINO) | `FP32` | machine learning |
|
||||
|
||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const config = {
|
||||
url: 'https://docs.immich.app',
|
||||
baseUrl: '/',
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
favicon: 'img/favicon.png',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
@@ -29,6 +28,9 @@ const config = {
|
||||
// Mermaid diagrams
|
||||
markdown: {
|
||||
mermaid: true,
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
},
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.wrangler]]
|
||||
version = "4.66.0"
|
||||
backend = "npm:wrangler"
|
||||
+2
-2
@@ -3,7 +3,7 @@ run = "pnpm install --filter documentation --frozen-lockfile"
|
||||
|
||||
[tasks.start]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "docusaurus --port 3005"
|
||||
run = "docusaurus start --port 3005"
|
||||
|
||||
[tasks.build]
|
||||
env._.path = "./node_modules/.bin"
|
||||
@@ -28,4 +28,4 @@ run = "prettier --write ."
|
||||
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
|
||||
|
||||
[tools]
|
||||
wrangler = "4.66.0"
|
||||
wrangler = "4.91.0"
|
||||
|
||||
@@ -83,9 +83,7 @@ volumes:
|
||||
model_cache:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
pnpm_cache:
|
||||
pnpm_store_server:
|
||||
pnpm_store_web:
|
||||
build_cache:
|
||||
server_node_modules:
|
||||
web_node_modules:
|
||||
github_node_modules:
|
||||
|
||||
+16
-1
@@ -1,11 +1,21 @@
|
||||
[tasks.install]
|
||||
run = "pnpm install --filter immich-e2e --frozen-lockfile"
|
||||
|
||||
[tasks.build]
|
||||
dir = "{{ config_root }}"
|
||||
run = "docker compose build"
|
||||
|
||||
[tasks.test]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "vitest --run"
|
||||
|
||||
[tasks.playwright-install]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright install"
|
||||
|
||||
[tasks."test-web"]
|
||||
depends = ["//e2e:build", "//e2e:ci-setup", "//e2e:playwright-install"]
|
||||
env._.path = "./node_modules/.bin"
|
||||
run = "playwright test"
|
||||
|
||||
@@ -30,7 +40,12 @@ run = "tsc --noEmit"
|
||||
|
||||
|
||||
[tasks.ci-setup]
|
||||
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
|
||||
depends = [
|
||||
"//:sdk:install",
|
||||
"//:sdk:build",
|
||||
"//packages/cli:install",
|
||||
"//packages/cli:build",
|
||||
]
|
||||
run = { task = ":install" }
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@socket.io/component-emitter": "^3.1.2",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/node": "^24.12.4",
|
||||
"@types/pg": "^8.15.1",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^7.0.0",
|
||||
|
||||
@@ -55,8 +55,8 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
|
||||
result.duration.push(asset.duration);
|
||||
result.projectionType.push(asset.projectionType);
|
||||
result.livePhotoVideoId.push(asset.livePhotoVideoId);
|
||||
result.city.push(asset.city);
|
||||
result.country.push(asset.country);
|
||||
result.city?.push(asset.city);
|
||||
result.country?.push(asset.country);
|
||||
result.visibility.push(asset.visibility);
|
||||
}
|
||||
|
||||
|
||||
@@ -536,7 +536,7 @@ test.describe('Timeline', () => {
|
||||
force: false,
|
||||
ids: [assetToTrash.id],
|
||||
});
|
||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
||||
await page.locator('#control-bar').getByLabel('Close').click();
|
||||
await page.getByText('Trash', { exact: true }).click();
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
await thumbnailUtils.expectInViewport(page, assetToTrash.id);
|
||||
@@ -676,7 +676,7 @@ test.describe('Timeline', () => {
|
||||
ids: [assetToArchive.id],
|
||||
});
|
||||
await thumbnailUtils.expectThumbnailIsArchive(page, assetToArchive.id);
|
||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
||||
await page.locator('#control-bar').getByLabel('Close').click();
|
||||
await page.getByRole('link').getByText('Archive').click();
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
await thumbnailUtils.expectInViewport(page, assetToArchive.id);
|
||||
@@ -823,7 +823,7 @@ test.describe('Timeline', () => {
|
||||
});
|
||||
// ensure thumbnail still exists and has favorite icon
|
||||
await thumbnailUtils.expectThumbnailIsFavorite(page, assetToFavorite.id);
|
||||
await page.locator('#asset-selection-app-bar').getByLabel('Close').click();
|
||||
await page.locator('#control-bar').getByLabel('Close').click();
|
||||
await page.getByRole('link').getByText('Favorites').click();
|
||||
await timelineUtils.waitForTimelineLoad(page);
|
||||
await pageUtils.goToAsset(page, assetToFavorite.fileCreatedAt);
|
||||
|
||||
+12
-15
@@ -465,14 +465,10 @@
|
||||
"advanced_settings_proxy_headers_title": "Custom proxy headers [EXPERIMENTAL]",
|
||||
"advanced_settings_readonly_mode_subtitle": "Enables the read-only mode where the photos can be only viewed, things like selecting multiple images, sharing, casting, delete are all disabled. Enable/Disable read-only via user avatar from the main screen",
|
||||
"advanced_settings_readonly_mode_title": "Read-only mode",
|
||||
"advanced_settings_review_remote_deletions_subtitle": "Manually review cloud trash changes. Restorations are applied automatically.",
|
||||
"advanced_settings_review_remote_deletions_title": "Review remote deletions",
|
||||
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates [EXPERIMENTAL]",
|
||||
"advanced_settings_sync_remote_deletions_off_subtitle": "Cloud trash changes are ignored",
|
||||
"advanced_settings_sync_remote_deletions_selector_title": "Sync remote deletions [EXPERIMENTAL]",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatically move assets to trash or restore them on this device when that action is taken on the web.",
|
||||
"advanced_settings_sync_remote_deletions_title": "Auto sync",
|
||||
"advanced_settings_sync_remote_deletions_subtitle": "Automatically delete or restore an asset on this device when that action is taken on the web",
|
||||
"advanced_settings_sync_remote_deletions_title": "Sync remote deletions [EXPERIMENTAL]",
|
||||
"advanced_settings_tile_subtitle": "Advanced user's settings",
|
||||
"advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting",
|
||||
"advanced_settings_troubleshooting_title": "Troubleshooting",
|
||||
@@ -583,11 +579,6 @@
|
||||
"asset_not_found_on_icloud": "Asset not found on iCloud. the asset may be inaccessible due to bad file stored on iCloud",
|
||||
"asset_offline": "Asset Offline",
|
||||
"asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.",
|
||||
"asset_out_of_sync_title": "Out-of-sync assets list",
|
||||
"asset_out_of_sync_trash_confirmation_text": "Move {count, plural, one {asset} other {# assets}} to your device trash?",
|
||||
"asset_out_of_sync_trash_confirmation_title": "Sync trash change",
|
||||
"asset_out_of_sync_trash_subtitle": "Assets moved to the Immich cloud trash: choose to move them to local trash or keep them on this device.",
|
||||
"asset_out_of_sync_trash_subtitle_result": "Nothing left to review — all decisions applied.",
|
||||
"asset_restored_successfully": "Asset restored successfully",
|
||||
"asset_skipped": "Skipped",
|
||||
"asset_skipped_in_trash": "In trash",
|
||||
@@ -606,7 +597,6 @@
|
||||
"assets_count": "{count, plural, one {# asset} other {# assets}}",
|
||||
"assets_deleted_permanently": "{count} asset(s) deleted permanently",
|
||||
"assets_deleted_permanently_from_server": "{count} asset(s) deleted permanently from the Immich server",
|
||||
"assets_denied_to_moved_to_trash_count": "Keeping local copies of {count, plural, one {# asset} other {# assets}}",
|
||||
"assets_downloaded_failed": "{count, plural, one {Downloaded # file - {error} file failed} other {Downloaded # files - {error} files failed}}",
|
||||
"assets_downloaded_successfully": "{count, plural, one {Downloaded # file successfully} other {Downloaded # files successfully}}",
|
||||
"assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash",
|
||||
@@ -708,6 +698,7 @@
|
||||
"birthdate_saved": "Date of birth saved successfully",
|
||||
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
||||
"blurred_background": "Blurred background",
|
||||
"browse_templates": "Browse templates",
|
||||
"bugs_and_feature_requests": "Bugs & Feature Requests",
|
||||
"build": "Build",
|
||||
"build_image": "Build Image",
|
||||
@@ -849,6 +840,7 @@
|
||||
"copy_error": "Copy error",
|
||||
"copy_file_path": "Copy file path",
|
||||
"copy_image": "Copy Image",
|
||||
"copy_json": "Copy JSON",
|
||||
"copy_link": "Copy link",
|
||||
"copy_link_to_clipboard": "Copy link to clipboard",
|
||||
"copy_password": "Copy password",
|
||||
@@ -986,7 +978,10 @@
|
||||
"downloading_asset_filename": "Downloading asset {filename}",
|
||||
"downloading_from_icloud": "Downloading from iCloud",
|
||||
"downloading_media": "Downloading media",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"drop_files_to_upload": "Drop files anywhere to upload",
|
||||
"duplicate": "Duplicate",
|
||||
"duplicate_workflow": "Duplicate workflow",
|
||||
"duplicates": "Duplicates",
|
||||
"duplicates_description": "Resolve each group by indicating which, if any, are duplicates.",
|
||||
"duration": "Duration",
|
||||
@@ -1376,7 +1371,6 @@
|
||||
"keep_all": "Keep All",
|
||||
"keep_description": "Choose what stays on your device when freeing up space.",
|
||||
"keep_favorites": "Keep favorites",
|
||||
"keep_in_trash": "Keep in trash",
|
||||
"keep_on_device": "Keep on device",
|
||||
"keep_on_device_hint": "Select items to keep on this device",
|
||||
"keep_this_delete_others": "Keep this, delete others",
|
||||
@@ -1696,7 +1690,6 @@
|
||||
"obtainium_configurator": "Obtainium Configurator",
|
||||
"obtainium_configurator_instructions": "Use Obtainium to install and update the Android app directly from Immich GitHub's release. Create an API key and select a variant to create your Obtainium configuration link",
|
||||
"ocr": "OCR",
|
||||
"off": "Off",
|
||||
"official_immich_resources": "Official Immich Resources",
|
||||
"offline": "Offline",
|
||||
"offset": "Offset",
|
||||
@@ -1971,7 +1964,6 @@
|
||||
"retry_upload": "Retry upload",
|
||||
"review_duplicates": "Review duplicates",
|
||||
"review_large_files": "Review large files",
|
||||
"review_out_of_sync_changes": "Review out-of-sync changes",
|
||||
"role": "Role",
|
||||
"role_editor": "Editor",
|
||||
"role_viewer": "Viewer",
|
||||
@@ -2241,6 +2233,7 @@
|
||||
"slideshow_repeat": "Repeat slideshow",
|
||||
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
|
||||
"slideshow_settings": "Slideshow settings",
|
||||
"smart_album": "Smart album",
|
||||
"sort_albums_by": "Sort albums by...",
|
||||
"sort_created": "Date created",
|
||||
"sort_items": "Number of items",
|
||||
@@ -2267,6 +2260,7 @@
|
||||
"step_delete_confirm": "Are you sure you want to delete this step?",
|
||||
"step_details": "Step details",
|
||||
"steps": "Steps",
|
||||
"steps_count": "{count, plural, one {# step} other {# steps}}",
|
||||
"stop_casting": "Stop casting",
|
||||
"stop_motion_photo": "Stop Motion Photo",
|
||||
"stop_photo_sharing": "Stop sharing your photos?",
|
||||
@@ -2428,6 +2422,7 @@
|
||||
"use_browser_locale_description": "Format dates, times, and numbers based on your browser locale",
|
||||
"use_current_connection": "Use current connection",
|
||||
"use_custom_date_range": "Use custom date range instead",
|
||||
"use_template": "Use template",
|
||||
"user": "User",
|
||||
"user_has_been_deleted": "This user has been deleted.",
|
||||
"user_id": "User ID",
|
||||
@@ -2489,6 +2484,7 @@
|
||||
"week": "Week",
|
||||
"welcome": "Welcome",
|
||||
"welcome_to_immich": "Welcome to Immich",
|
||||
"when": "When",
|
||||
"width": "Width",
|
||||
"wifi_name": "Wi-Fi Name",
|
||||
"workflow": "Workflow",
|
||||
@@ -2501,6 +2497,7 @@
|
||||
"workflow_name": "Workflow name",
|
||||
"workflow_navigation_prompt": "Are you sure you want to leave without saving your changes?",
|
||||
"workflow_summary": "Workflow summary",
|
||||
"workflow_templates": "Workflow templates",
|
||||
"workflow_update_success": "Workflow updated successfully",
|
||||
"workflow_updated": "Workflow updated",
|
||||
"workflows": "Workflows",
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from socket import socket
|
||||
|
||||
from gunicorn.arbiter import Arbiter
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
@@ -42,6 +42,10 @@ class MaxBatchSize(BaseModel):
|
||||
ocr: int | None = None
|
||||
|
||||
|
||||
def default_worker_timeout() -> int:
|
||||
return 900 if os.environ.get("DEVICE") == "rocm" else 300
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="MACHINE_LEARNING_",
|
||||
@@ -54,7 +58,7 @@ class Settings(BaseSettings):
|
||||
model_ttl: int = 300
|
||||
model_ttl_poll_s: int = 10
|
||||
workers: int = 1
|
||||
worker_timeout: int = 300
|
||||
worker_timeout: int = Field(default_factory=default_worker_timeout)
|
||||
http_keepalive_timeout_s: int = 2
|
||||
test_full: bool = False
|
||||
request_threads: int = os.cpu_count() or 4
|
||||
|
||||
@@ -89,4 +89,10 @@ class FaceRecognizer(InferenceModel):
|
||||
@property
|
||||
def _batch_size_default(self) -> int | None:
|
||||
providers = ort.get_available_providers()
|
||||
return None if self.model_format == ModelFormat.ONNX and "OpenVINOExecutionProvider" not in providers else 1
|
||||
if (
|
||||
self.model_format == ModelFormat.ONNX
|
||||
and "MIGraphXExecutionProvider" not in providers
|
||||
and "OpenVINOExecutionProvider" not in providers
|
||||
):
|
||||
return None
|
||||
return 1
|
||||
|
||||
@@ -64,6 +64,7 @@ class TextRecognizer(InferenceModel):
|
||||
rec_batch_num=max_batch_size if max_batch_size else 6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
lang_type=self.language,
|
||||
model_root_dir=self.cache_dir,
|
||||
)
|
||||
)
|
||||
return session
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
@@ -12,6 +13,37 @@ from immich_ml.schemas import ModelPrecision, SessionNode
|
||||
|
||||
from ..config import log, settings
|
||||
|
||||
MigraphxInputSignature = tuple[tuple[str, str, tuple[int, ...]], ...]
|
||||
|
||||
_migraphx_registry_lock = Lock()
|
||||
_migraphx_model_locks: dict[str, Lock] = {}
|
||||
_migraphx_compiled_inputs: set[tuple[str, MigraphxInputSignature]] = set()
|
||||
|
||||
|
||||
def _migraphx_get_model_lock(model_key: str) -> Lock:
|
||||
with _migraphx_registry_lock:
|
||||
lock = _migraphx_model_locks.get(model_key)
|
||||
if lock is None:
|
||||
lock = Lock()
|
||||
_migraphx_model_locks[model_key] = lock
|
||||
return lock
|
||||
|
||||
|
||||
def _migraphx_has_compiled_input(key: tuple[str, MigraphxInputSignature]) -> bool:
|
||||
with _migraphx_registry_lock:
|
||||
return key in _migraphx_compiled_inputs
|
||||
|
||||
|
||||
def _migraphx_mark_compiled_input(key: tuple[str, MigraphxInputSignature]) -> None:
|
||||
with _migraphx_registry_lock:
|
||||
_migraphx_compiled_inputs.add(key)
|
||||
|
||||
|
||||
def _migraphx_input_signature(
|
||||
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
|
||||
) -> MigraphxInputSignature:
|
||||
return tuple((name, str(value.dtype), tuple(value.shape)) for name, value in sorted(input_feed.items()))
|
||||
|
||||
|
||||
class OrtSession:
|
||||
session: ort.InferenceSession
|
||||
@@ -48,7 +80,21 @@ class OrtSession:
|
||||
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
|
||||
run_options: Any = None,
|
||||
) -> list[NDArray[np.float32]]:
|
||||
outputs: list[NDArray[np.float32]] = self.session.run(output_names, input_feed, run_options)
|
||||
if "MIGraphXExecutionProvider" in self.providers:
|
||||
model_key = self.model_path.resolve().as_posix()
|
||||
input_key = (model_key, _migraphx_input_signature(input_feed))
|
||||
if not _migraphx_has_compiled_input(input_key):
|
||||
model_lock = _migraphx_get_model_lock(model_key)
|
||||
with model_lock:
|
||||
if not _migraphx_has_compiled_input(input_key):
|
||||
outputs: list[NDArray[np.float32]] = self.session.run(output_names, input_feed, run_options)
|
||||
_migraphx_mark_compiled_input(input_key)
|
||||
return outputs
|
||||
|
||||
outputs = self.session.run(output_names, input_feed, run_options)
|
||||
return outputs
|
||||
|
||||
outputs = self.session.run(output_names, input_feed, run_options)
|
||||
return outputs
|
||||
|
||||
@property
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.python]]
|
||||
version = "3.11.15"
|
||||
backend = "core:python"
|
||||
|
||||
[tools.python."platforms.linux-arm64"]
|
||||
checksum = "sha256:243f794278eff6adba96ed3677ec6877175df84c25f140e17f09f9be82d0f12a"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:52b4c52094ff8b383a45c694acf4c5c0e883152be6d5229a35a8186ce907c6eb"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-x64"]
|
||||
checksum = "sha256:171dffd8c0f66e8a0725364a7428015b22fc18dd298b24f541392e17dd0e561f"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:2ac90fef8917ebd14826a6d667593a06cf0ae5f745ba9b1147dc086dd35f5284"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-unknown-linux-musl-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.macos-arm64"]
|
||||
checksum = "sha256:fdfc363b538662eb7441a14e06f72c4a992c56af7f401f5730ea5081f8f8ad6e"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-aarch64-apple-darwin-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.macos-x64"]
|
||||
checksum = "sha256:5f1eb247cbca2c0ad5ccbf6d299a4f54b31b5c63b492d74c3531dc4344a42f88"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-apple-darwin-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.python."platforms.windows-x64"]
|
||||
checksum = "sha256:756d7f148498b8822f6aedf44a020613576f09983161f346ad36dcef6238cdc3"
|
||||
url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260510/cpython-3.11.15+20260510-x86_64-pc-windows-msvc-install_only_stripped.tar.gz"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.uv]]
|
||||
version = "0.8.15"
|
||||
backend = "aqua:astral-sh/uv"
|
||||
|
||||
[tools.uv."platforms.linux-arm64"]
|
||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
|
||||
[tools.uv."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:23ea21a05c62c4c307ce691f29bff2f15c94c4f07f2b83d9b356f0664bc8b3a2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
|
||||
[tools.uv."platforms.linux-x64"]
|
||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
|
||||
[tools.uv."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:d0fec58f3124e05e0a1af0f6541abfce4333253cdaf23c7b6bb2e6128bf138ea"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
|
||||
[tools.uv."platforms.macos-arm64"]
|
||||
checksum = "sha256:103367962c5cb00bf7370d84cbaa3fec5a9807be9cc833ea9d8eea400c119fa2"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-aarch64-apple-darwin.tar.gz"
|
||||
|
||||
[tools.uv."platforms.macos-x64"]
|
||||
checksum = "sha256:2bbef70982e97dfc36454de173f35ec1a5e83ae11e3885df6a50db3fd76171cb"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-apple-darwin.tar.gz"
|
||||
|
||||
[tools.uv."platforms.windows-x64"]
|
||||
checksum = "sha256:459d95892a5cc5c21779532f4f41b9238594b79e312a5142da2148ecfa10e705"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.8.15/uv-x86_64-pc-windows-msvc.zip"
|
||||
@@ -10,7 +10,7 @@ dependencies = [
|
||||
"fastapi>=0.95.2,<1.0",
|
||||
"gunicorn>=21.1.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",
|
||||
"opencv-python-headless>=4.7.0.72,<5.0",
|
||||
"orjson>=3.9.5",
|
||||
|
||||
@@ -35,7 +35,37 @@ from immich_ml.sessions.ort import OrtSession
|
||||
from immich_ml.sessions.rknn import RknnSession, run_inference
|
||||
|
||||
|
||||
class FakeLock:
|
||||
def __init__(self) -> None:
|
||||
self.enter = mock.Mock()
|
||||
self.exit = mock.Mock()
|
||||
|
||||
def __enter__(self) -> None:
|
||||
self.enter()
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
self.exit(*args)
|
||||
|
||||
|
||||
class TestBase:
|
||||
def test_sets_default_worker_timeout(self, monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("DEVICE", raising=False)
|
||||
monkeypatch.delenv("MACHINE_LEARNING_WORKER_TIMEOUT", raising=False)
|
||||
|
||||
assert Settings().worker_timeout == 300
|
||||
|
||||
def test_sets_rocm_default_worker_timeout(self, monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("DEVICE", "rocm")
|
||||
monkeypatch.delenv("MACHINE_LEARNING_WORKER_TIMEOUT", raising=False)
|
||||
|
||||
assert Settings().worker_timeout == 900
|
||||
|
||||
def test_worker_timeout_env_override(self, monkeypatch: MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("DEVICE", "rocm")
|
||||
monkeypatch.setenv("MACHINE_LEARNING_WORKER_TIMEOUT", "1200")
|
||||
|
||||
assert Settings().worker_timeout == 1200
|
||||
|
||||
def test_sets_default_cache_dir(self) -> None:
|
||||
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
|
||||
|
||||
@@ -413,6 +443,52 @@ class TestOrtSession:
|
||||
|
||||
assert sess_options is session.sess_options
|
||||
|
||||
def test_serializes_rocm_first_run_for_new_input_signature(self, mocker: MockerFixture) -> None:
|
||||
lock = FakeLock()
|
||||
get_model_lock = mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
|
||||
mocker.patch("immich_ml.sessions.ort._migraphx_compiled_inputs", set())
|
||||
mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["MIGraphXExecutionProvider"])
|
||||
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
|
||||
|
||||
session.run(None, input_feed)
|
||||
session.run(None, input_feed)
|
||||
|
||||
lock.enter.assert_called_once()
|
||||
lock.exit.assert_called_once()
|
||||
get_model_lock.assert_called_once()
|
||||
session.session.run.assert_has_calls([mock.call(None, input_feed, None), mock.call(None, input_feed, None)])
|
||||
|
||||
def test_serializes_rocm_run_for_each_new_input_signature(self, mocker: MockerFixture) -> None:
|
||||
lock = FakeLock()
|
||||
mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
|
||||
mocker.patch("immich_ml.sessions.ort._migraphx_compiled_inputs", set())
|
||||
mocker.patch("immich_ml.sessions.ort.Path.mkdir")
|
||||
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["MIGraphXExecutionProvider"])
|
||||
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
|
||||
new_shape_input_feed = {"input": np.random.rand(2, 3, 224, 224).astype(np.float32)}
|
||||
|
||||
session.run(None, input_feed)
|
||||
session.run(None, new_shape_input_feed)
|
||||
|
||||
assert lock.enter.call_count == 2
|
||||
assert lock.exit.call_count == 2
|
||||
session.session.run.assert_has_calls(
|
||||
[mock.call(None, input_feed, None), mock.call(None, new_shape_input_feed, None)]
|
||||
)
|
||||
|
||||
def test_does_not_serialize_non_rocm_run(self, mocker: MockerFixture) -> None:
|
||||
lock = FakeLock()
|
||||
get_model_lock = mocker.patch("immich_ml.sessions.ort._migraphx_get_model_lock", return_value=lock)
|
||||
session = OrtSession("/cache/ViT-B-32__openai/model.onnx", providers=["CPUExecutionProvider"])
|
||||
input_feed = {"input": np.random.rand(1, 3, 224, 224).astype(np.float32)}
|
||||
|
||||
session.run(None, input_feed)
|
||||
|
||||
get_model_lock.assert_not_called()
|
||||
lock.enter.assert_not_called()
|
||||
session.session.run.assert_called_once_with(None, input_feed, None)
|
||||
|
||||
|
||||
class TestAnnSession:
|
||||
def test_creates_ann_session(self, ann_session: mock.Mock, info: mock.Mock) -> None:
|
||||
@@ -883,6 +959,34 @@ class TestFaceRecognition:
|
||||
onnx.load.assert_not_called()
|
||||
onnx.save.assert_not_called()
|
||||
|
||||
def test_recognition_does_not_add_batch_axis_for_migraphx(
|
||||
self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
|
||||
) -> None:
|
||||
onnx = mocker.patch("immich_ml.models.facial_recognition.recognition.onnx", autospec=True)
|
||||
update_dims = mocker.patch(
|
||||
"immich_ml.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
|
||||
)
|
||||
mocker.patch("immich_ml.models.base.InferenceModel.download")
|
||||
mocker.patch("immich_ml.models.facial_recognition.recognition.ArcFaceONNX")
|
||||
mocker.patch(
|
||||
"immich_ml.models.facial_recognition.recognition.ort.get_available_providers",
|
||||
return_value=["MIGraphXExecutionProvider", "CPUExecutionProvider"],
|
||||
)
|
||||
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
|
||||
|
||||
inputs = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
|
||||
outputs = [SimpleNamespace(name="output.1", shape=(1, 800))]
|
||||
ort_session.return_value.get_inputs.return_value = inputs
|
||||
ort_session.return_value.get_outputs.return_value = outputs
|
||||
|
||||
face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
|
||||
face_recognizer.load()
|
||||
|
||||
assert face_recognizer.batch_size == 1
|
||||
update_dims.assert_not_called()
|
||||
onnx.load.assert_not_called()
|
||||
onnx.save.assert_not_called()
|
||||
|
||||
def test_set_custom_max_batch_size(self, mocker: MockerFixture) -> None:
|
||||
mocker.patch.object(settings, "max_batch_size", MaxBatchSize(facial_recognition=2))
|
||||
|
||||
@@ -924,7 +1028,12 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
)
|
||||
|
||||
def test_set_custom_max_batch_size(self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture) -> None:
|
||||
@@ -937,7 +1046,12 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=4, rec_img_shape=(3, 48, 320))
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=4,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
)
|
||||
|
||||
def test_ignore_other_custom_max_batch_size(
|
||||
@@ -952,7 +1066,12 @@ class TestOcr:
|
||||
text_recognizer.load()
|
||||
|
||||
rapid_recognizer.assert_called_once_with(
|
||||
OcrOptions(session=ort_session.return_value, rec_batch_num=6, rec_img_shape=(3, 48, 320))
|
||||
OcrOptions(
|
||||
session=ort_session.return_value,
|
||||
rec_batch_num=6,
|
||||
rec_img_shape=(3, 48, 320),
|
||||
model_root_dir=text_recognizer.cache_dir,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1004,7 +1004,7 @@ requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
|
||||
{ name = "gunicorn", specifier = ">=21.1.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 = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
|
||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools."aqua:flutter/flutter"]]
|
||||
version = "3.44.0"
|
||||
backend = "aqua:flutter/flutter"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-arm64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.linux-x64-musl"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.44.0-stable.tar.xz"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-arm64"]
|
||||
checksum = "blake3:fb03aa5d9790205c948922ec3f0751c16e4575b09d6ae9dd4fbeb664a69f0e00"
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_arm64_3.44.0-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.macos-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos_3.44.0-stable.zip"
|
||||
|
||||
[tools."aqua:flutter/flutter"."platforms.windows-x64"]
|
||||
url = "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.44.0-stable.zip"
|
||||
|
||||
[[tools.flutter]]
|
||||
version = "3.41.9-stable"
|
||||
backend = "asdf:flutter"
|
||||
|
||||
[[tools."github:CQLabs/homebrew-dcm"]]
|
||||
version = "1.37.0"
|
||||
backend = "github:CQLabs/homebrew-dcm"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64"]
|
||||
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
|
||||
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
|
||||
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
|
||||
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
|
||||
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
|
||||
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
|
||||
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
|
||||
|
||||
[[tools."github:extism/cli"]]
|
||||
version = "1.6.3"
|
||||
backend = "github:extism/cli"
|
||||
|
||||
[tools."github:extism/cli"."platforms.linux-arm64"]
|
||||
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
||||
|
||||
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
|
||||
|
||||
[tools."github:extism/cli"."platforms.linux-x64"]
|
||||
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
||||
|
||||
[tools."github:extism/cli"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
|
||||
|
||||
[tools."github:extism/cli"."platforms.macos-arm64"]
|
||||
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694029"
|
||||
|
||||
[tools."github:extism/cli"."platforms.macos-x64"]
|
||||
checksum = "sha256:9a2f71b6e6009685a622cc3084e52d2a1a8e23c98d29ffa72e666e9dc699855f"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-amd64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694026"
|
||||
|
||||
[tools."github:extism/cli"."platforms.windows-x64"]
|
||||
checksum = "sha256:47e4ed2782445b2b08a4d1ac127211588f8b4d1fc25fd6481d4cb65151b5213c"
|
||||
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-windows-amd64.zip"
|
||||
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
|
||||
|
||||
[[tools."github:extism/js-pdk"]]
|
||||
version = "1.6.0"
|
||||
backend = "github:extism/js-pdk"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.linux-arm64"]
|
||||
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.linux-x64"]
|
||||
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
|
||||
checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.macos-x64"]
|
||||
checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239"
|
||||
|
||||
[tools."github:extism/js-pdk"."platforms.windows-x64"]
|
||||
checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc"
|
||||
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz"
|
||||
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
|
||||
|
||||
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
|
||||
version = "7.1.3-6"
|
||||
backend = "github:jellyfin/jellyfin-ffmpeg"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
|
||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
|
||||
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
|
||||
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
|
||||
|
||||
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
|
||||
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
|
||||
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
|
||||
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
|
||||
|
||||
[[tools."github:webassembly/binaryen"]]
|
||||
version = "version_124"
|
||||
backend = "github:webassembly/binaryen"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.linux-arm64"]
|
||||
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
|
||||
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
|
||||
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926134"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.macos-x64"]
|
||||
checksum = "sha256:b389bb0731758d86c3cb266d01d28a12725c23bd3cabc3df34faa162af0887e9"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-macos.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926135"
|
||||
|
||||
[tools."github:webassembly/binaryen"."platforms.windows-x64"]
|
||||
checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2203"
|
||||
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
|
||||
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
|
||||
|
||||
[[tools.java]]
|
||||
version = "21.0.2"
|
||||
backend = "core:java"
|
||||
|
||||
[tools.java."platforms.linux-arm64"]
|
||||
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.linux-x64"]
|
||||
checksum = "sha256:a2def047a73941e01a73739f92755f86b895811afb1f91243db214cff5bdac3f"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_linux-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-arm64"]
|
||||
checksum = "sha256:b3d588e16ec1e0ef9805d8a696591bd518a5cea62567da8f53b5ce32d11d22e4"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-aarch64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.macos-x64"]
|
||||
checksum = "sha256:8fd09e15dc406387a0aba70bf5d99692874e999bf9cd9208b452b5d76ac922d3"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_macos-x64_bin.tar.gz"
|
||||
|
||||
[tools.java."platforms.windows-x64"]
|
||||
checksum = "sha256:b6c17e747ae78cdd6de4d7532b3164b277daee97c007d3eaa2b39cca99882664"
|
||||
url = "https://download.java.net/java/GA/jdk21.0.2/f2283984656d49d69e91c558476027ac/13/GPL/openjdk-21.0.2_windows-x64_bin.zip"
|
||||
|
||||
[[tools.node]]
|
||||
version = "24.15.0"
|
||||
backend = "core:node"
|
||||
|
||||
[tools.node."platforms.linux-arm64"]
|
||||
checksum = "sha256:73afc234d558c24919875f51c2d1ea002a2ada4ea6f83601a383869fefa64eed"
|
||||
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:31e98aa960a067da91edffd5d93bc46657b5d2a8029612c359f5f2ac0060152a"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-arm64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64"]
|
||||
checksum = "sha256:44836872d9aec49f1e6b52a9a922872db9a2b02d235a616a5681b6a85fec8d89"
|
||||
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:f55af5bd489c5347b113ca6594cae00a54b30ba57ac5875324311bfc6f4762e3"
|
||||
url = "https://unofficial-builds.nodejs.org/download/release/v24.15.0/node-v24.15.0-linux-x64-musl.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-arm64"]
|
||||
checksum = "sha256:372331b969779ab5d15b949884fc6eaf88d5afe87bde8ba881d6400b9100ffc4"
|
||||
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-arm64.tar.gz"
|
||||
|
||||
[tools.node."platforms.macos-x64"]
|
||||
checksum = "sha256:ffd5ee293467927f3ee731a553eb88fd1f48cf74eebc2d74a6babe4af228673b"
|
||||
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-darwin-x64.tar.gz"
|
||||
|
||||
[tools.node."platforms.windows-x64"]
|
||||
checksum = "sha256:cc5149eabd53779ce1e7bdc5401643622d0c7e6800ade18928a767e940bb0e62"
|
||||
url = "https://nodejs.org/dist/v24.15.0/node-v24.15.0-win-x64.zip"
|
||||
|
||||
[[tools."npm:oazapfts"]]
|
||||
version = "7.5.0"
|
||||
backend = "npm:oazapfts"
|
||||
|
||||
[[tools.opentofu]]
|
||||
version = "1.11.6"
|
||||
backend = "aqua:opentofu/opentofu"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64"]
|
||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:d4f2ab15776925864b049bb329d69682851de6f5204f256e9fa86d07a0308850"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64"]
|
||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:02800fafa2753a9f50c38483e2fdf5bc353fd62895eb9e25eec9a5145df3a69e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_linux_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.macos-arm64"]
|
||||
checksum = "sha256:62d7fa8539e13b444827aa0a3b90c5972da5c47e8f8882d9dcf2e430e78840c1"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.macos-x64"]
|
||||
checksum = "sha256:1408cdef1c380f914565e6b4bb70794c6b163f195fcb233357f3d6c5745906b6"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.opentofu."platforms.windows-x64"]
|
||||
checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c7077367e"
|
||||
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
|
||||
|
||||
[[tools.pnpm]]
|
||||
version = "10.33.4"
|
||||
backend = "aqua:pnpm/pnpm"
|
||||
|
||||
[[tools.terragrunt]]
|
||||
version = "1.0.3"
|
||||
backend = "aqua:gruntwork-io/terragrunt"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-arm64-musl"]
|
||||
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.linux-x64-musl"]
|
||||
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.macos-arm64"]
|
||||
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.macos-x64"]
|
||||
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz"
|
||||
|
||||
[tools.terragrunt."platforms.windows-x64"]
|
||||
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db"
|
||||
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz"
|
||||
@@ -16,8 +16,8 @@ config_roots = [
|
||||
|
||||
[tools]
|
||||
node = "24.15.0"
|
||||
flutter = "3.41.9"
|
||||
pnpm = "10.33.1"
|
||||
"aqua:flutter/flutter" = "3.44.0"
|
||||
pnpm = "10.33.4"
|
||||
terragrunt = "1.0.3"
|
||||
opentofu = "1.11.6"
|
||||
java = "21.0.2"
|
||||
@@ -50,11 +50,12 @@ macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
|
||||
[settings]
|
||||
experimental = true
|
||||
pin = true
|
||||
lockfile = true
|
||||
|
||||
[tasks.plugins]
|
||||
run = [
|
||||
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||
"pnpm --filter @immich/plugin-sdk --filter @immich/plugin-core build"
|
||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core install --frozen-lockfile",
|
||||
"pnpm --filter @immich/sdk --filter @immich/plugin-sdk --filter @immich/plugin-core build",
|
||||
]
|
||||
|
||||
[tasks.open-api-typescript]
|
||||
@@ -72,18 +73,83 @@ run = "bash ./bin/generate-dart-sdk.sh"
|
||||
env = { SHARP_IGNORE_GLOBAL_LIBVIPS = true }
|
||||
run = [
|
||||
{ task = "//:plugins" },
|
||||
{ task = "//server:build" },
|
||||
{ task = "//server:install" },
|
||||
{ task = "//server:build" },
|
||||
{ task = "//server:sync-open-api" },
|
||||
{ task = ":open-api-typescript"},
|
||||
{ task = ":open-api-dart"},
|
||||
{ task = ":open-api-typescript" },
|
||||
{ task = ":open-api-dart" },
|
||||
]
|
||||
|
||||
[tasks.sql]
|
||||
dir = "server"
|
||||
run = "node ./dist/bin/sync-sql.js"
|
||||
|
||||
# TODO dev, prod, and e2e should be de-duplicated by using env but for some reason I ran into issues
|
||||
[tasks.dev]
|
||||
depends = "//:plugins"
|
||||
dir = "docker"
|
||||
interactive = true
|
||||
env = { COMPOSE_BAKE = true }
|
||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
||||
depends_post = "//:dev-down"
|
||||
|
||||
[tasks.dev-update]
|
||||
run = { task = "//:dev", args = ["--build", "-V"] }
|
||||
|
||||
[tasks.dev-scale]
|
||||
run = { task = "//:dev", args = ["--build", "-V", "--scale immich-server=3"] }
|
||||
|
||||
[tasks.dev-down]
|
||||
dir = "docker"
|
||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
||||
|
||||
[tasks.prod]
|
||||
depends = "//:plugins"
|
||||
dir = "docker"
|
||||
interactive = true
|
||||
env = { COMPOSE_BAKE = true }
|
||||
run = "docker compose -f ./docker-compose.prod.yml up --build --remove-orphans"
|
||||
depends_post = "//:prod-down"
|
||||
|
||||
[tasks.prod-scale]
|
||||
run = { task = "//:prod", args = [
|
||||
"--build",
|
||||
"-V",
|
||||
"--scale immich-server=3",
|
||||
"--scale immich-microservices",
|
||||
] }
|
||||
|
||||
[tasks.prod-down]
|
||||
dir = "docker"
|
||||
run = "docker compose -f ./docker-compose.prod.yml down --remove-orphans"
|
||||
|
||||
[tasks.e2e]
|
||||
depends = "//:plugins"
|
||||
dir = "e2e"
|
||||
interactive = true
|
||||
env = { COMPOSE_BAKE = true }
|
||||
run = "docker compose -f ./docker-compose.yml up --remove-orphans"
|
||||
depends_post = "//:e2e-down"
|
||||
|
||||
[tasks.e2e-dev]
|
||||
depends = "//:plugins"
|
||||
dir = "e2e"
|
||||
interactive = true
|
||||
env = { COMPOSE_BAKE = true }
|
||||
run = "docker compose -f ./docker-compose.dev.yml up --remove-orphans"
|
||||
depends_post = "//:e2e-dev-down"
|
||||
|
||||
[tasks.e2e-update]
|
||||
run = { task = "//:e2e", args = ["--build", '-V'] }
|
||||
|
||||
[tasks.e2e-down]
|
||||
dir = "e2e"
|
||||
run = "docker compose -f ./docker-compose.yml down --remove-orphans"
|
||||
|
||||
[tasks.e2e-dev-down]
|
||||
dir = "e2e"
|
||||
run = "docker compose -f ./docker-compose.dev.yml down --remove-orphans"
|
||||
|
||||
# SDK tasks
|
||||
[tasks."sdk:install"]
|
||||
dir = "packages/sdk"
|
||||
@@ -99,3 +165,14 @@ run = "pnpm format"
|
||||
|
||||
[tasks."i18n:format-fix"]
|
||||
run = "pnpm format:fix"
|
||||
|
||||
[tasks.clean]
|
||||
run = [
|
||||
"find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'dist' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'build' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.svelte-kit' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name 'coverage' -type d -prune -exec rm -rf '{}' +",
|
||||
"find . -name '.pnpm-store' -type d -prune -exec rm -rf '{}' +",
|
||||
{ task = "//:*-down" },
|
||||
]
|
||||
|
||||
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.41.9",
|
||||
"dart.lineLength": 120,
|
||||
"[dart]": {
|
||||
"editor.rulers": [
|
||||
|
||||
@@ -17,6 +17,8 @@ import app.alextran.immich.images.LocalImageApi
|
||||
import app.alextran.immich.images.LocalImagesImpl
|
||||
import app.alextran.immich.images.RemoteImageApi
|
||||
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.NativeSyncApiImpl26
|
||||
import app.alextran.immich.sync.NativeSyncApiImpl30
|
||||
@@ -44,7 +46,9 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
} else {
|
||||
NativeSyncApiImpl30(ctx)
|
||||
}
|
||||
val permissionApiImpl = PermissionApiImpl(ctx)
|
||||
NativeSyncApi.setUp(messenger, nativeSyncApiImpl)
|
||||
PermissionApi.setUp(messenger, permissionApiImpl)
|
||||
LocalImageApi.setUp(messenger, LocalImagesImpl(ctx))
|
||||
RemoteImageApi.setUp(messenger, RemoteImagesImpl(ctx))
|
||||
|
||||
@@ -53,6 +57,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
flutterEngine.plugins.add(backgroundEngineLockImpl)
|
||||
flutterEngine.plugins.add(nativeSyncApiImpl)
|
||||
flutterEngine.plugins.add(permissionApiImpl)
|
||||
}
|
||||
|
||||
fun cancelPlugins(flutterEngine: FlutterEngine) {
|
||||
@@ -60,6 +65,8 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
flutterEngine.plugins.get(NativeSyncApiImpl26::class.java) as ImmichPlugin?
|
||||
?: flutterEngine.plugins.get(NativeSyncApiImpl30::class.java) as ImmichPlugin?
|
||||
nativeApi?.detachFromEngine()
|
||||
val permissionApi = flutterEngine.plugins.get(PermissionApiImpl::class.java) as ImmichPlugin?
|
||||
permissionApi?.detachFromEngine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ interface NetworkApi {
|
||||
fun hasCertificate(): Boolean
|
||||
fun getClientPointer(): Long
|
||||
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
|
||||
fun getAppGroupId(): String
|
||||
|
||||
companion object {
|
||||
/** The codec used by NetworkApi. */
|
||||
@@ -430,6 +431,21 @@ interface NetworkApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.getAppGroupId())
|
||||
} catch (exception: Throwable) {
|
||||
NetworkPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
|
||||
private var networkApi: NetworkApiImpl? = null
|
||||
|
||||
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
networkApi = NetworkApiImpl()
|
||||
networkApi = NetworkApiImpl(binding.applicationContext)
|
||||
NetworkApi.setUp(binding.binaryMessenger, networkApi)
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
|
||||
}
|
||||
}
|
||||
|
||||
private class NetworkApiImpl : NetworkApi {
|
||||
private class NetworkApiImpl(private val context: Context) : NetworkApi {
|
||||
var activity: Activity? = null
|
||||
|
||||
override fun getAppGroupId(): String = context.packageName
|
||||
|
||||
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
|
||||
try {
|
||||
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
|
||||
|
||||
+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()
|
||||
}
|
||||
}
|
||||
@@ -9,26 +9,19 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.PluginRegistry
|
||||
|
||||
private const val TAG = "MediaTrashDelegate"
|
||||
|
||||
class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListener {
|
||||
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
|
||||
|
||||
companion object {
|
||||
private const val PERMISSION_REQUEST_CODE = 1001
|
||||
private const val TRASH_REQUEST_CODE = 1002
|
||||
}
|
||||
|
||||
fun hasManageMediaPermission(): Boolean {
|
||||
private fun hasManageMediaPermission(): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
MediaStore.canManageMedia(ctx)
|
||||
} else {
|
||||
@@ -36,38 +29,6 @@ class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListen
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
activity.startActivityForResult(intent, PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
|
||||
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)))
|
||||
@@ -76,11 +37,7 @@ class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListen
|
||||
|
||||
val id = mediaId.toLongOrNull()
|
||||
if (id == null) {
|
||||
callback(
|
||||
Result.failure(
|
||||
FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)
|
||||
)
|
||||
)
|
||||
callback(Result.failure(FlutterError("INVALID_ID", "The file id is not a valid number: $mediaId", null)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -89,24 +46,13 @@ class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListen
|
||||
return
|
||||
}
|
||||
|
||||
restoreUris(listOf(ContentUris.withAppendedId(contentUriForType(type.toInt()), id)), callback)
|
||||
restoreUri(ContentUris.withAppendedId(contentUriForType(type.toInt()), id), callback)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun restoreUris(uris: List<Uri>, callback: (Result<Boolean>) -> Unit) {
|
||||
if (uris.isEmpty()) {
|
||||
callback(Result.failure(FlutterError("TRASH_ERROR", "No URIs to restore", null)))
|
||||
return
|
||||
}
|
||||
|
||||
toggleTrash(uris, false, callback)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private fun toggleTrash(
|
||||
contentUris: List<Uri>,
|
||||
isTrashed: Boolean,
|
||||
callback: (Result<Boolean>) -> Unit
|
||||
private fun restoreUri(
|
||||
contentUri: Uri,
|
||||
callback: (Result<Boolean>) -> Unit,
|
||||
) {
|
||||
val activity = activityBinding?.activity
|
||||
if (activity == null) {
|
||||
@@ -115,19 +61,23 @@ class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListen
|
||||
}
|
||||
|
||||
try {
|
||||
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, contentUris, isTrashed)
|
||||
val pendingIntent = MediaStore.createTrashRequest(ctx.contentResolver, listOf(contentUri), false)
|
||||
pendingResult = callback
|
||||
activity.startIntentSenderForResult(
|
||||
pendingIntent.intentSender,
|
||||
TRASH_REQUEST_CODE,
|
||||
trashRequestCode,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
0,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error creating or starting trash request", e)
|
||||
callback(Result.failure(FlutterError("TRASH_ERROR", "Error creating or starting trash request", null)))
|
||||
pendingResult = null
|
||||
callback(
|
||||
Result.failure(
|
||||
FlutterError("TRASH_ERROR", "Error creating or starting trash request", e.toString())
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,23 +109,25 @@ class MediaTrashDelegate(context: Context) : PluginRegistry.ActivityResultListen
|
||||
}
|
||||
|
||||
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 == PERMISSION_REQUEST_CODE) {
|
||||
pendingResult?.invoke(Result.success(hasManageMediaPermission()))
|
||||
pendingResult = null
|
||||
return true
|
||||
}
|
||||
|
||||
if (requestCode == TRASH_REQUEST_CODE) {
|
||||
pendingResult?.invoke(Result.success(resultCode == Activity.RESULT_OK))
|
||||
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,9 +553,6 @@ interface NativeSyncApi {
|
||||
fun hashAssets(assetIds: List<String>, allowNetworkAccess: Boolean, callback: (Result<List<HashResult>>) -> Unit)
|
||||
fun cancelHashing()
|
||||
fun getTrashedAssets(): Map<String, List<PlatformAsset>>
|
||||
fun hasManageMediaPermission(): Boolean
|
||||
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit)
|
||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit)
|
||||
fun getCloudIdForAssetIds(assetIds: List<String>): List<CloudIdResult>
|
||||
|
||||
@@ -751,57 +748,6 @@ interface NativeSyncApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hasManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
listOf(api.hasManageMediaPermission())
|
||||
} catch (exception: Throwable) {
|
||||
MessagesPigeonUtils.wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.requestManageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.requestManageMediaPermission{ 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 {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.manageMediaPermission$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.manageMediaPermission{ 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 {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
|
||||
@@ -451,16 +451,6 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin(), ActivityAwa
|
||||
hashTask = null
|
||||
}
|
||||
|
||||
fun hasManageMediaPermission(): Boolean = mediaTrashDelegate.hasManageMediaPermission()
|
||||
|
||||
fun requestManageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||
mediaTrashDelegate.requestManageMediaPermission { completeWhenActive(callback, it) }
|
||||
}
|
||||
|
||||
fun manageMediaPermission(callback: (Result<Boolean>) -> Unit) {
|
||||
mediaTrashDelegate.manageMediaPermission { completeWhenActive(callback, it) }
|
||||
}
|
||||
|
||||
fun restoreFromTrashById(mediaId: String, type: Long, callback: (Result<Boolean>) -> Unit) {
|
||||
mediaTrashDelegate.restoreFromTrashById(mediaId, type) { completeWhenActive(callback, it) }
|
||||
}
|
||||
|
||||
@@ -5,3 +5,7 @@ android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
+73
-406
@@ -2498,104 +2498,6 @@
|
||||
"name": "trashed_local_asset_entity",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "checksum",
|
||||
"getter_name": "checksum",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "is_favorite",
|
||||
"getter_name": "isFavorite",
|
||||
"moor_type": "bool",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
|
||||
"dialectAwareDefaultConstraints": {
|
||||
"sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
|
||||
},
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "orientation",
|
||||
"getter_name": "orientation",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "i_cloud_id",
|
||||
"getter_name": "iCloudId",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "adjustment_time",
|
||||
"getter_name": "adjustmentTime",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "latitude",
|
||||
"getter_name": "latitude",
|
||||
"moor_type": "double",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "longitude",
|
||||
"getter_name": "longitude",
|
||||
"moor_type": "double",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "playback_style",
|
||||
"getter_name": "playbackStyle",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<AssetPlaybackStyle>(AssetPlaybackStyle.values)",
|
||||
"dart_type_name": "AssetPlaybackStyle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"getter_name": "name",
|
||||
@@ -2670,6 +2572,16 @@
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "album_id",
|
||||
"getter_name": "albumId",
|
||||
@@ -2680,6 +2592,40 @@
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "checksum",
|
||||
"getter_name": "checksum",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "is_favorite",
|
||||
"getter_name": "isFavorite",
|
||||
"moor_type": "bool",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
|
||||
"dialectAwareDefaultConstraints": {
|
||||
"sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
|
||||
},
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "orientation",
|
||||
"getter_name": "orientation",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"getter_name": "source",
|
||||
@@ -2693,6 +2639,20 @@
|
||||
"dart_expr": "const EnumIndexConverter<TrashOrigin>(TrashOrigin.values)",
|
||||
"dart_type_name": "TrashOrigin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "playback_style",
|
||||
"getter_name": "playbackStyle",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<AssetPlaybackStyle>(AssetPlaybackStyle.values)",
|
||||
"dart_type_name": "AssetPlaybackStyle"
|
||||
}
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
@@ -2804,7 +2764,7 @@
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "metadata",
|
||||
"name": "settings",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
@@ -2849,244 +2809,6 @@
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"references": [],
|
||||
"type": "table",
|
||||
"data": {
|
||||
"name": "trash_sync_entity",
|
||||
"was_declared_in_moor": false,
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"getter_name": "id",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "checksum",
|
||||
"getter_name": "checksum",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "is_favorite",
|
||||
"getter_name": "isFavorite",
|
||||
"moor_type": "bool",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
|
||||
"dialectAwareDefaultConstraints": {
|
||||
"sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
|
||||
},
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "orientation",
|
||||
"getter_name": "orientation",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "i_cloud_id",
|
||||
"getter_name": "iCloudId",
|
||||
"moor_type": "string",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "adjustment_time",
|
||||
"getter_name": "adjustmentTime",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "latitude",
|
||||
"getter_name": "latitude",
|
||||
"moor_type": "double",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "longitude",
|
||||
"getter_name": "longitude",
|
||||
"moor_type": "double",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "playback_style",
|
||||
"getter_name": "playbackStyle",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('0')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<AssetPlaybackStyle>(AssetPlaybackStyle.values)",
|
||||
"dart_type_name": "AssetPlaybackStyle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"getter_name": "name",
|
||||
"moor_type": "string",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"getter_name": "type",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<AssetType>(AssetType.values)",
|
||||
"dart_type_name": "AssetType"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"getter_name": "createdAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"getter_name": "updatedAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "width",
|
||||
"getter_name": "width",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "height",
|
||||
"getter_name": "height",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "duration_ms",
|
||||
"getter_name": "durationMs",
|
||||
"moor_type": "int",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "decision",
|
||||
"getter_name": "decision",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<TrashStateDecision>(TrashStateDecision.values)",
|
||||
"dart_type_name": "TrashStateDecision"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "trigger_source",
|
||||
"getter_name": "triggerSource",
|
||||
"moor_type": "int",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": [],
|
||||
"type_converter": {
|
||||
"dart_expr": "const EnumIndexConverter<TrashTriggerSource>(TrashTriggerSource.values)",
|
||||
"dart_type_name": "TrashTriggerSource"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "remote_deleted_at",
|
||||
"getter_name": "remoteDeletedAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": true,
|
||||
"customConstraints": null,
|
||||
"default_dart": null,
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
},
|
||||
{
|
||||
"name": "decided_at",
|
||||
"getter_name": "decidedAt",
|
||||
"moor_type": "dateTime",
|
||||
"nullable": false,
|
||||
"customConstraints": null,
|
||||
"default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
|
||||
"default_client_dart": null,
|
||||
"dsl_features": []
|
||||
}
|
||||
],
|
||||
"is_virtual": false,
|
||||
"without_rowid": true,
|
||||
"constraints": [],
|
||||
"strict": true,
|
||||
"explicit_pk": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 32,
|
||||
"references": [
|
||||
18
|
||||
],
|
||||
@@ -3100,7 +2822,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 33,
|
||||
"id": 32,
|
||||
"references": [
|
||||
19
|
||||
],
|
||||
@@ -3114,7 +2836,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 34,
|
||||
"id": 33,
|
||||
"references": [
|
||||
19
|
||||
],
|
||||
@@ -3128,7 +2850,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 35,
|
||||
"id": 34,
|
||||
"references": [
|
||||
20
|
||||
],
|
||||
@@ -3142,7 +2864,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"id": 35,
|
||||
"references": [
|
||||
22
|
||||
],
|
||||
@@ -3156,7 +2878,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"id": 36,
|
||||
"references": [
|
||||
25
|
||||
],
|
||||
@@ -3170,7 +2892,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 38,
|
||||
"id": 37,
|
||||
"references": [
|
||||
26
|
||||
],
|
||||
@@ -3184,7 +2906,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"id": 38,
|
||||
"references": [
|
||||
26
|
||||
],
|
||||
@@ -3198,7 +2920,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"id": 39,
|
||||
"references": [
|
||||
26
|
||||
],
|
||||
@@ -3212,7 +2934,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 41,
|
||||
"id": 40,
|
||||
"references": [
|
||||
28
|
||||
],
|
||||
@@ -3226,7 +2948,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 42,
|
||||
"id": 41,
|
||||
"references": [
|
||||
28
|
||||
],
|
||||
@@ -3240,7 +2962,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 43,
|
||||
"id": 42,
|
||||
"references": [
|
||||
29
|
||||
],
|
||||
@@ -3252,34 +2974,6 @@
|
||||
"unique": false,
|
||||
"columns": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"references": [
|
||||
31
|
||||
],
|
||||
"type": "index",
|
||||
"data": {
|
||||
"on": 31,
|
||||
"name": "idx_trash_sync_decision",
|
||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trash_sync_decision ON trash_sync_entity (decision)",
|
||||
"unique": false,
|
||||
"columns": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"references": [
|
||||
31
|
||||
],
|
||||
"type": "index",
|
||||
"data": {
|
||||
"on": 31,
|
||||
"name": "idx_trash_sync_checksum",
|
||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync_entity (checksum)",
|
||||
"unique": false,
|
||||
"columns": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"fixed_sql": [
|
||||
@@ -3540,7 +3234,7 @@
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, \"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"album_id\" TEXT NOT NULL, \"source\" INTEGER NOT NULL, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;"
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3554,20 +3248,11 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "metadata",
|
||||
"name": "settings",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"metadata\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "trash_sync_entity",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"trash_sync_entity\" (\"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, \"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"decision\" INTEGER NOT NULL, \"trigger_source\" INTEGER NOT NULL, \"remote_deleted_at\" TEXT NULL, \"decided_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
|
||||
"sql": "CREATE TABLE IF NOT EXISTS \"settings\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3678,24 +3363,6 @@
|
||||
"sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_trash_sync_decision",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trash_sync_decision ON trash_sync_entity (decision)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_trash_sync_checksum",
|
||||
"sql": [
|
||||
{
|
||||
"dialect": "sqlite",
|
||||
"sql": "CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync_entity (checksum)"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,58 +1,23 @@
|
||||
PODS:
|
||||
- background_downloader (0.0.1):
|
||||
- Flutter
|
||||
- bonsoir_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- connectivity_plus (0.0.1):
|
||||
- Flutter
|
||||
- cupertino_http (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- device_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (2.4.3):
|
||||
- Flutter
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- flutter_udid (0.0.1):
|
||||
- Flutter
|
||||
- KeychainAccess
|
||||
- flutter_web_auth_2 (5.0.0):
|
||||
- Flutter
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- geolocator_apple (1.2.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- home_widget (0.0.1):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- KeychainAccess (4.2.2)
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- MapLibre (6.14.0)
|
||||
- maplibre_gl (0.0.1):
|
||||
- Flutter
|
||||
- MapLibre (= 6.14.0)
|
||||
- native_video_player (1.0.0):
|
||||
- Flutter
|
||||
- network_info_plus (0.0.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- photo_manager (3.9.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- share_handler_ios (0.0.14):
|
||||
- Flutter
|
||||
- share_handler_ios/share_handler_ios_models (= 0.0.14)
|
||||
@@ -61,144 +26,56 @@ PODS:
|
||||
- Flutter
|
||||
- share_handler_ios_models
|
||||
- share_handler_ios_models (0.0.9)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- background_downloader (from `.symlinks/plugins/background_downloader/ios`)
|
||||
- bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`)
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
|
||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
||||
- home_widget (from `.symlinks/plugins/home_widget/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
||||
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
|
||||
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- KeychainAccess
|
||||
- MapLibre
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
background_downloader:
|
||||
:path: ".symlinks/plugins/background_downloader/ios"
|
||||
bonsoir_darwin:
|
||||
:path: ".symlinks/plugins/bonsoir_darwin/darwin"
|
||||
connectivity_plus:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
cupertino_http:
|
||||
:path: ".symlinks/plugins/cupertino_http/darwin"
|
||||
device_info_plus:
|
||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||
flutter_udid:
|
||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||
flutter_web_auth_2:
|
||||
:path: ".symlinks/plugins/flutter_web_auth_2/ios"
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
geolocator_apple:
|
||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
||||
home_widget:
|
||||
:path: ".symlinks/plugins/home_widget/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
maplibre_gl:
|
||||
:path: ".symlinks/plugins/maplibre_gl/ios"
|
||||
native_video_player:
|
||||
:path: ".symlinks/plugins/native_video_player/ios"
|
||||
network_info_plus:
|
||||
:path: ".symlinks/plugins/network_info_plus/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
photo_manager:
|
||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
||||
share_handler_ios:
|
||||
:path: ".symlinks/plugins/share_handler_ios/ios"
|
||||
share_handler_ios_models:
|
||||
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad
|
||||
bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e
|
||||
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
|
||||
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
|
||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
|
||||
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
|
||||
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
|
||||
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
|
||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
||||
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
|
||||
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B21E34AB2E5B09100031FDB9 /* BackgroundWorker.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 */; };
|
||||
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 */; };
|
||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
||||
F02538E92DFBCBDD008C3FA3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
@@ -37,6 +39,7 @@
|
||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -105,6 +108,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
@@ -125,6 +130,7 @@
|
||||
FE5499F72F1198DE006016CB /* RemoteImagesImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteImagesImpl.swift; sourceTree = "<group>"; };
|
||||
FE5FE4AD2F30FBC000A71243 /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
|
||||
FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbhash.swift; sourceTree = "<group>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
@@ -189,6 +195,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
||||
@@ -243,6 +250,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -283,6 +291,7 @@
|
||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||
B2EE00052E72CA15008B6CA7 /* Permission */,
|
||||
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
|
||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
@@ -317,6 +326,15 @@
|
||||
path = Connectivity;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2EE00052E72CA15008B6CA7 /* Permission */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B2EE00032E72CA15008B6CA7 /* PermissionApiImpl.swift */,
|
||||
B2EE00012E72CA15008B6CA7 /* PermissionApi.g.swift */,
|
||||
);
|
||||
path = Permission;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FAC6F8B62D287F120078CB2F /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -346,6 +364,9 @@
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
@@ -449,6 +470,7 @@
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
||||
);
|
||||
@@ -619,6 +641,8 @@
|
||||
FE5499F42F1197D8006016CB /* RemoteImages.g.swift in Sources */,
|
||||
FE5FE4AE2F30FBC000A71243 /* ImageProcessing.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 */,
|
||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */,
|
||||
B25D377C2E72CA26008B6CA7 /* ConnectivityApiImpl.swift in Sources */,
|
||||
@@ -718,6 +742,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share.profile;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -750,7 +775,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -801,6 +825,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share.debug;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -860,6 +885,7 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -894,7 +920,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -924,7 +949,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -1080,7 +1104,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1124,7 +1147,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1165,7 +1187,6 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2W7AC6T8T5;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -1272,7 +1293,17 @@
|
||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
||||
productName = StructuredFieldValues;
|
||||
};
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
+19
-2
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -19,6 +18,24 @@
|
||||
"version" : "7.8.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "maplibre-gl-native-distribution",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||
"state" : {
|
||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
||||
"version" : "6.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sqlite-data",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -146,5 +163,5 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
"version" : 2
|
||||
}
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$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>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -19,6 +18,24 @@
|
||||
"version" : "7.9.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "keychainaccess",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kishikawakatsumi/KeychainAccess",
|
||||
"state" : {
|
||||
"revision" : "84e546727d66f1adc5439debad16270d0fdd04e7",
|
||||
"version" : "4.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "maplibre-gl-native-distribution",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git",
|
||||
"state" : {
|
||||
"revision" : "60d9bb85c94ce6e7fc4406cd32529fd12bdb7809",
|
||||
"version" : "6.14.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sqlite-data",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -146,5 +163,5 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
"version" : 2
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import native_video_player
|
||||
|
||||
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
|
||||
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
|
||||
PermissionApiSetup.setUp(binaryMessenger: messenger, api: PermissionApiImpl())
|
||||
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
|
||||
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
|
||||
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
|
||||
|
||||
Generated
+14
@@ -288,6 +288,7 @@ protocol NetworkApi {
|
||||
func hasCertificate() throws -> Bool
|
||||
func getClientPointer() throws -> Int64
|
||||
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
|
||||
func getAppGroupId() throws -> String
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
@@ -388,5 +389,18 @@ class NetworkApiSetup {
|
||||
} else {
|
||||
setRequestHeadersChannel.setMessageHandler(nil)
|
||||
}
|
||||
let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
getAppGroupIdChannel.setMessageHandler { _, reply in
|
||||
do {
|
||||
let result = try api.getAppGroupId()
|
||||
reply(wrapResult(result))
|
||||
} catch {
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getAppGroupIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ class NetworkApiImpl: NetworkApi {
|
||||
return Int64(Int(bitPattern: pointer))
|
||||
}
|
||||
|
||||
func getAppGroupId() throws -> String {
|
||||
return Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||
}
|
||||
|
||||
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
|
||||
URLSessionManager.setServerUrls(serverUrls)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import native_video_player
|
||||
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
|
||||
let HEADERS_KEY = "immich.request_headers"
|
||||
let SERVER_URLS_KEY = "immich.server_urls"
|
||||
let APP_GROUP = "group.app.immich.share"
|
||||
let APP_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||
let COOKIE_EXPIRY_DAYS: TimeInterval = 400
|
||||
|
||||
enum AuthCookie: CaseIterable {
|
||||
|
||||
+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))
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.immich.share</string>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.immich.share</string>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
Generated
+7
-57
@@ -537,10 +537,7 @@ protocol NativeSyncApi {
|
||||
func hashAssets(assetIds: [String], allowNetworkAccess: Bool, completion: @escaping (Result<[HashResult], Error>) -> Void)
|
||||
func cancelHashing() throws
|
||||
func getTrashedAssets() throws -> [String: [PlatformAsset]]
|
||||
func hasManageMediaPermission() throws -> Bool
|
||||
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func restoreFromTrashById(mediaId: String, type: Int64, completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
func getCloudIdForAssetIds(assetIds: [String]) throws -> [CloudIdResult]
|
||||
}
|
||||
|
||||
@@ -725,60 +722,13 @@ class NativeSyncApiSetup {
|
||||
} else {
|
||||
getTrashedAssetsChannel.setMessageHandler(nil)
|
||||
}
|
||||
let hasManageMediaPermissionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.hasManageMediaPermission\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
let restoreFromTrashByIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.restoreFromTrashById\(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.NativeSyncApi.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.NativeSyncApi.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)
|
||||
}
|
||||
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
|
||||
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
|
||||
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))
|
||||
@@ -788,7 +738,7 @@ class NativeSyncApiSetup {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
restoreFromTrashByIdChannel.setMessageHandler(nil)
|
||||
restoreFromTrashByIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
let getCloudIdForAssetIdsChannel = taskQueue == nil
|
||||
? FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NativeSyncApi.getCloudIdForAssetIds\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
|
||||
@@ -383,20 +383,8 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||
}
|
||||
|
||||
func hasManageMediaPermission() throws -> Bool {
|
||||
throw PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)
|
||||
}
|
||||
|
||||
func requestManageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
completion(.failure(PigeonError(code: "UNSUPPORTED_OS", message: "This feature not supported on iOS.", details: nil)))
|
||||
}
|
||||
|
||||
func manageMediaPermission(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
completion(.failure(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(.failure(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> {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.immich.share</string>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -2,7 +2,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
let IMMICH_SHARE_GROUP = "group.app.immich.share"
|
||||
let IMMICH_SHARE_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
|
||||
|
||||
enum WidgetError: Error, Codable {
|
||||
case noLogin
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.immich.share</string>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -21,6 +21,7 @@ platform :ios do
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
|
||||
BASE_BUNDLE_ID = "app.alextran.immich"
|
||||
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
|
||||
DEV_GROUP_ID = "group.app.immich.share.testflight"
|
||||
|
||||
# Helper method to get App Store Connect API key
|
||||
def get_api_key
|
||||
@@ -33,6 +34,13 @@ platform :ios do
|
||||
)
|
||||
end
|
||||
|
||||
# Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override
|
||||
def build_xcargs(group_id: nil)
|
||||
args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual"
|
||||
args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id
|
||||
args
|
||||
end
|
||||
|
||||
# Helper method to get version from pubspec.yaml
|
||||
def get_version_from_pubspec
|
||||
require 'yaml'
|
||||
@@ -89,7 +97,8 @@ end
|
||||
version_number: nil,
|
||||
profile_name_main:,
|
||||
profile_name_share:,
|
||||
profile_name_widget:
|
||||
profile_name_widget:,
|
||||
group_id: nil
|
||||
)
|
||||
app_identifier = base_bundle_id
|
||||
|
||||
@@ -97,7 +106,7 @@ end
|
||||
if version_number
|
||||
increment_version_number(version_number: version_number)
|
||||
end
|
||||
|
||||
|
||||
# Increment build number
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number(
|
||||
@@ -106,14 +115,14 @@ end
|
||||
) + 1,
|
||||
xcodeproj: "./Runner.xcodeproj"
|
||||
)
|
||||
|
||||
|
||||
# Build the app
|
||||
build_app(
|
||||
scheme: "Runner",
|
||||
workspace: "Runner.xcworkspace",
|
||||
configuration: configuration,
|
||||
export_method: "app-store",
|
||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
||||
xcargs: build_xcargs(group_id: group_id),
|
||||
export_options: {
|
||||
provisioningProfiles: {
|
||||
"#{app_identifier}" => profile_name_main,
|
||||
@@ -165,7 +174,8 @@ end
|
||||
distribute_external: false,
|
||||
profile_name_main: main_profile_name,
|
||||
profile_name_share: share_profile_name,
|
||||
profile_name_widget: widget_profile_name
|
||||
profile_name_widget: widget_profile_name,
|
||||
group_id: DEV_GROUP_ID
|
||||
)
|
||||
end
|
||||
|
||||
@@ -274,7 +284,7 @@ end
|
||||
configuration: "Release",
|
||||
export_method: "app-store",
|
||||
skip_package_ipa: true,
|
||||
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
|
||||
xcargs: build_xcargs(group_id: DEV_GROUP_ID),
|
||||
export_options: {
|
||||
provisioningProfiles: {
|
||||
DEV_BUNDLE_ID => main_profile_name,
|
||||
|
||||
@@ -23,6 +23,7 @@ const String kBackupLivePhotoGroup = 'backup_live_photo_group';
|
||||
const String kDownloadGroupImage = 'group_image';
|
||||
const String kDownloadGroupVideo = 'group_video';
|
||||
const String kDownloadGroupLivePhoto = 'group_livephoto';
|
||||
const String kShareDownloadGroup = 'group_share';
|
||||
|
||||
// Timeline constants
|
||||
const int kTimelineNoneSegmentSize = 120;
|
||||
@@ -30,7 +31,6 @@ const int kTimelineAssetLoadBatchSize = 1024;
|
||||
const int kTimelineAssetLoadOppositeSize = 64;
|
||||
|
||||
// Widget keys
|
||||
const String appShareGroupId = "group.app.immich.share";
|
||||
const String kWidgetAuthToken = "widget_auth_token";
|
||||
const String kWidgetServerEndpoint = "widget_server_url";
|
||||
const String kWidgetCustomHeaders = "widget_custom_headers";
|
||||
|
||||
@@ -31,7 +31,7 @@ class LocalAsset extends BaseAsset {
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
super.isEdited = false,
|
||||
required super.isEdited,
|
||||
}) : remoteAssetId = remoteId;
|
||||
|
||||
@override
|
||||
|
||||
@@ -23,7 +23,6 @@ class RemoteAsset extends BaseAsset {
|
||||
required super.createdAt,
|
||||
required super.updatedAt,
|
||||
this.uploadedAt,
|
||||
this.deletedAt,
|
||||
super.width,
|
||||
super.height,
|
||||
super.durationMs,
|
||||
@@ -33,6 +32,7 @@ class RemoteAsset extends BaseAsset {
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
required super.isEdited,
|
||||
this.deletedAt,
|
||||
}) : localAssetId = localId;
|
||||
|
||||
@override
|
||||
@@ -62,7 +62,6 @@ class RemoteAsset extends BaseAsset {
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
uploadedAt: ${uploadedAt ?? "<NA>"},
|
||||
deletedAt: ${deletedAt ?? "<NA>"},
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationMs: ${durationMs ?? "<NA>"},
|
||||
@@ -90,7 +89,6 @@ class RemoteAsset extends BaseAsset {
|
||||
ownerId == other.ownerId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility &&
|
||||
deletedAt == other.deletedAt &&
|
||||
stackId == other.stackId &&
|
||||
uploadedAt == other.uploadedAt &&
|
||||
deletedAt == other.deletedAt;
|
||||
@@ -104,7 +102,6 @@ class RemoteAsset extends BaseAsset {
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode ^
|
||||
deletedAt.hashCode ^
|
||||
stackId.hashCode ^
|
||||
uploadedAt.hashCode ^
|
||||
deletedAt.hashCode;
|
||||
@@ -119,7 +116,6 @@ class RemoteAsset extends BaseAsset {
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? uploadedAt,
|
||||
DateTime? deletedAt,
|
||||
int? width,
|
||||
int? height,
|
||||
int? durationMs,
|
||||
@@ -129,6 +125,7 @@ class RemoteAsset extends BaseAsset {
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
bool? isEdited,
|
||||
DateTime? deletedAt,
|
||||
}) {
|
||||
return RemoteAsset(
|
||||
id: id ?? this.id,
|
||||
@@ -140,7 +137,6 @@ class RemoteAsset extends BaseAsset {
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
uploadedAt: uploadedAt ?? this.uploadedAt,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
width: width ?? this.width,
|
||||
height: height ?? this.height,
|
||||
durationMs: durationMs ?? this.durationMs,
|
||||
@@ -150,6 +146,7 @@ class RemoteAsset extends BaseAsset {
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
isEdited: isEdited ?? this.isEdited,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
|
||||
class RemoteDeletedLocalAsset {
|
||||
final LocalAsset asset;
|
||||
final DateTime remoteDeletedAt;
|
||||
|
||||
const RemoteDeletedLocalAsset({required this.asset, required this.remoteDeletedAt});
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/config/album_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/backup_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/image_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/map_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/network_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/slideshow_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/timeline_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/viewer_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
const defaultConfig = AppConfig();
|
||||
|
||||
class AppConfig {
|
||||
final LogLevel logLevel;
|
||||
final ThemeConfig theme;
|
||||
final CleanupConfig cleanup;
|
||||
final MapConfig map;
|
||||
@@ -18,8 +29,10 @@ class AppConfig {
|
||||
final SlideshowConfig slideshow;
|
||||
final AlbumConfig album;
|
||||
final BackupConfig backup;
|
||||
final NetworkConfig network;
|
||||
|
||||
const AppConfig({
|
||||
this.logLevel = .info,
|
||||
this.theme = const .new(),
|
||||
this.cleanup = const .new(),
|
||||
this.map = const .new(),
|
||||
@@ -29,9 +42,11 @@ class AppConfig {
|
||||
this.slideshow = const .new(),
|
||||
this.album = const .new(),
|
||||
this.backup = const .new(),
|
||||
this.network = const .new(),
|
||||
});
|
||||
|
||||
AppConfig copyWith({
|
||||
LogLevel? logLevel,
|
||||
ThemeConfig? theme,
|
||||
CleanupConfig? cleanup,
|
||||
MapConfig? map,
|
||||
@@ -41,7 +56,9 @@ class AppConfig {
|
||||
SlideshowConfig? slideshow,
|
||||
AlbumConfig? album,
|
||||
BackupConfig? backup,
|
||||
NetworkConfig? network,
|
||||
}) => .new(
|
||||
logLevel: logLevel ?? this.logLevel,
|
||||
theme: theme ?? this.theme,
|
||||
cleanup: cleanup ?? this.cleanup,
|
||||
map: map ?? this.map,
|
||||
@@ -51,12 +68,14 @@ class AppConfig {
|
||||
slideshow: slideshow ?? this.slideshow,
|
||||
album: album ?? this.album,
|
||||
backup: backup ?? this.backup,
|
||||
network: network ?? this.network,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is AppConfig &&
|
||||
other.logLevel == logLevel &&
|
||||
other.theme == theme &&
|
||||
other.cleanup == cleanup &&
|
||||
other.map == map &&
|
||||
@@ -65,12 +84,113 @@ class AppConfig {
|
||||
other.viewer == viewer &&
|
||||
other.slideshow == slideshow &&
|
||||
other.album == album &&
|
||||
other.backup == backup);
|
||||
other.backup == backup &&
|
||||
other.network == network);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup);
|
||||
int get hashCode =>
|
||||
Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)';
|
||||
'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)';
|
||||
|
||||
T read<T extends Object>(SettingsKey<T> key) =>
|
||||
(switch (key) {
|
||||
.logLevel => logLevel,
|
||||
.themePrimaryColor => theme.primaryColor,
|
||||
.themeMode => theme.mode,
|
||||
.themeDynamic => theme.dynamicTheme,
|
||||
.themeColorfulInterface => theme.colorfulInterface,
|
||||
.imagePreferRemote => image.preferRemote,
|
||||
.imageLoadOriginal => image.loadOriginal,
|
||||
.viewerLoopVideo => viewer.loopVideo,
|
||||
.viewerLoadOriginalVideo => viewer.loadOriginalVideo,
|
||||
.viewerAutoPlayVideo => viewer.autoPlayVideo,
|
||||
.viewerTapToNavigate => viewer.tapToNavigate,
|
||||
.networkAutoEndpointSwitching => network.autoEndpointSwitching,
|
||||
.networkPreferredWifiName => network.preferredWifiName,
|
||||
.networkLocalEndpoint => network.localEndpoint,
|
||||
.networkExternalEndpointList => network.externalEndpointList,
|
||||
.networkCustomHeaders => network.customHeaders,
|
||||
.albumSortMode => album.sortMode,
|
||||
.albumIsReverse => album.isReverse,
|
||||
.albumIsGrid => album.isGrid,
|
||||
.backupEnabled => backup.enabled,
|
||||
.backupUseCellularForVideos => backup.useCellularForVideos,
|
||||
.backupUseCellularForPhotos => backup.useCellularForPhotos,
|
||||
.backupRequireCharging => backup.requireCharging,
|
||||
.backupTriggerDelay => backup.triggerDelay,
|
||||
.backupSyncAlbums => backup.syncAlbums,
|
||||
.timelineTilesPerRow => timeline.tilesPerRow,
|
||||
.timelineGroupAssetsBy => timeline.groupAssetsBy,
|
||||
.timelineStorageIndicator => timeline.storageIndicator,
|
||||
.mapShowFavoriteOnly => map.favoritesOnly,
|
||||
.mapRelativeDate => map.relativeDays,
|
||||
.mapIncludeArchived => map.includeArchived,
|
||||
.mapThemeMode => map.themeMode,
|
||||
.mapWithPartners => map.withPartners,
|
||||
.cleanupKeepFavorites => cleanup.keepFavorites,
|
||||
.cleanupKeepMediaType => cleanup.keepMediaType,
|
||||
.cleanupKeepAlbumIds => cleanup.keepAlbumIds,
|
||||
.cleanupCutoffDaysAgo => cleanup.cutoffDaysAgo,
|
||||
.cleanupDefaultsInitialized => cleanup.defaultsInitialized,
|
||||
.slideshowTransition => slideshow.transition,
|
||||
.slideshowRepeat => slideshow.repeat,
|
||||
.slideshowDuration => slideshow.duration,
|
||||
.slideshowLook => slideshow.look,
|
||||
.slideshowDirection => slideshow.direction,
|
||||
})
|
||||
as T;
|
||||
|
||||
factory AppConfig.fromEntries(Map<SettingsKey<Object>, Object> overrides) =>
|
||||
overrides.entries.fold(const AppConfig(), (config, entry) => config.write(entry.key, entry.value));
|
||||
|
||||
AppConfig write<T extends Object>(SettingsKey<T> key, T value) {
|
||||
return switch (key) {
|
||||
.logLevel => copyWith(logLevel: value as LogLevel),
|
||||
.themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)),
|
||||
.themeMode => copyWith(theme: theme.copyWith(mode: value as ThemeMode)),
|
||||
.themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)),
|
||||
.themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)),
|
||||
.imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)),
|
||||
.imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)),
|
||||
.viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)),
|
||||
.viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)),
|
||||
.viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)),
|
||||
.viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)),
|
||||
.networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)),
|
||||
.networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))),
|
||||
.networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))),
|
||||
.networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List<String>)),
|
||||
.networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map<String, String>)),
|
||||
.albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)),
|
||||
.albumIsReverse => copyWith(album: album.copyWith(isReverse: value as bool)),
|
||||
.albumIsGrid => copyWith(album: album.copyWith(isGrid: value as bool)),
|
||||
.backupEnabled => copyWith(backup: backup.copyWith(enabled: value as bool)),
|
||||
.backupUseCellularForVideos => copyWith(backup: backup.copyWith(useCellularForVideos: value as bool)),
|
||||
.backupUseCellularForPhotos => copyWith(backup: backup.copyWith(useCellularForPhotos: value as bool)),
|
||||
.backupRequireCharging => copyWith(backup: backup.copyWith(requireCharging: value as bool)),
|
||||
.backupTriggerDelay => copyWith(backup: backup.copyWith(triggerDelay: value as int)),
|
||||
.backupSyncAlbums => copyWith(backup: backup.copyWith(syncAlbums: value as bool)),
|
||||
.timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)),
|
||||
.timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)),
|
||||
.timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)),
|
||||
.mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)),
|
||||
.mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)),
|
||||
.mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)),
|
||||
.mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)),
|
||||
.mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)),
|
||||
.cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)),
|
||||
.cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)),
|
||||
.cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List<String>)),
|
||||
.cleanupCutoffDaysAgo => copyWith(cleanup: cleanup.copyWith(cutoffDaysAgo: value as int)),
|
||||
.cleanupDefaultsInitialized => copyWith(cleanup: cleanup.copyWith(defaultsInitialized: value as bool)),
|
||||
.slideshowTransition => copyWith(slideshow: slideshow.copyWith(transition: value as bool)),
|
||||
.slideshowRepeat => copyWith(slideshow: slideshow.copyWith(repeat: value as bool)),
|
||||
.slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)),
|
||||
.slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)),
|
||||
.slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
class NetworkConfig {
|
||||
final bool autoEndpointSwitching;
|
||||
final String? preferredWifiName;
|
||||
final String? localEndpoint;
|
||||
final String preferredWifiName;
|
||||
final String localEndpoint;
|
||||
final List<String> externalEndpointList;
|
||||
final Map<String, String> customHeaders;
|
||||
|
||||
const NetworkConfig({
|
||||
this.autoEndpointSwitching = false,
|
||||
this.preferredWifiName,
|
||||
this.localEndpoint,
|
||||
this.preferredWifiName = '',
|
||||
this.localEndpoint = '',
|
||||
this.externalEndpointList = const [],
|
||||
this.customHeaders = const {},
|
||||
});
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:immich_mobile/domain/models/config/network_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
|
||||
class SystemConfig {
|
||||
final LogLevel logLevel;
|
||||
final NetworkConfig network;
|
||||
|
||||
const SystemConfig({this.logLevel = .info, this.network = const .new()});
|
||||
|
||||
SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) =>
|
||||
SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(logLevel, network);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)';
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
enum MetadataDomain<T extends Object> {
|
||||
appConfig<AppConfig>('config.app'),
|
||||
systemConfig<SystemConfig>('config.system');
|
||||
|
||||
final String prefix;
|
||||
const MetadataDomain(this.prefix);
|
||||
}
|
||||
|
||||
enum MetadataKey<T extends Object> {
|
||||
// Theme
|
||||
themePrimaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
|
||||
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
|
||||
themeDynamic<bool>(.appConfig, 'theme.dynamic', false),
|
||||
themeColorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
|
||||
|
||||
// Image
|
||||
imagePreferRemote<bool>(.appConfig, 'image.preferRemote', false),
|
||||
imageLoadOriginal<bool>(.appConfig, 'image.loadOriginal', false),
|
||||
|
||||
// Viewer
|
||||
viewerLoopVideo<bool>(.appConfig, 'viewer.loopVideo', true),
|
||||
viewerLoadOriginalVideo<bool>(.appConfig, 'viewer.loadOriginalVideo', false),
|
||||
viewerAutoPlayVideo<bool>(.appConfig, 'viewer.autoPlayVideo', true),
|
||||
viewerTapToNavigate<bool>(.appConfig, 'viewer.tapToNavigate', false),
|
||||
|
||||
// Network
|
||||
networkAutoEndpointSwitching<bool>(.systemConfig, 'network.autoEndpointSwitching', false),
|
||||
networkPreferredWifiName<String>(.systemConfig, 'network.preferredWifiName', ''),
|
||||
networkLocalEndpoint<String>(.systemConfig, 'network.localEndpoint', ''),
|
||||
networkExternalEndpointList<List<String>>(
|
||||
.systemConfig,
|
||||
'network.externalEndpointList',
|
||||
[],
|
||||
_ListCodec(_PrimitiveCodec.string),
|
||||
),
|
||||
networkCustomHeaders<Map<String, String>>(
|
||||
.systemConfig,
|
||||
'network.customHeaders',
|
||||
{},
|
||||
_MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string),
|
||||
),
|
||||
|
||||
// Album
|
||||
albumSortMode<AlbumSortMode>(
|
||||
.appConfig,
|
||||
'album.sortMode',
|
||||
AlbumSortMode.mostRecent,
|
||||
_EnumCodec(AlbumSortMode.values),
|
||||
),
|
||||
albumIsReverse<bool>(.appConfig, 'album.isReverse', true),
|
||||
albumIsGrid<bool>(.appConfig, 'album.isGrid', false),
|
||||
|
||||
// Backup
|
||||
backupEnabled<bool>(.appConfig, 'backup.enabled', false),
|
||||
backupUseCellularForVideos<bool>(.appConfig, 'backup.useCellularForVideos', false),
|
||||
backupUseCellularForPhotos<bool>(.appConfig, 'backup.useCellularForPhotos', false),
|
||||
backupRequireCharging<bool>(.appConfig, 'backup.requireCharging', false),
|
||||
backupTriggerDelay<int>(.appConfig, 'backup.triggerDelay', 30),
|
||||
backupSyncAlbums<bool>(.appConfig, 'backup.syncAlbums', false),
|
||||
|
||||
// Timeline
|
||||
timelineTilesPerRow<int>(.appConfig, 'timeline.tilesPerRow', 4),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(
|
||||
.appConfig,
|
||||
'timeline.groupAssetsBy',
|
||||
GroupAssetsBy.day,
|
||||
_EnumCodec(GroupAssetsBy.values),
|
||||
),
|
||||
timelineStorageIndicator<bool>(.appConfig, 'timeline.storageIndicator', true),
|
||||
|
||||
// Log
|
||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
|
||||
|
||||
// Map
|
||||
mapShowFavoriteOnly<bool>(.appConfig, 'map.showFavoriteOnly', false),
|
||||
mapRelativeDate<int>(.appConfig, 'map.relativeDate', 0),
|
||||
mapIncludeArchived<bool>(.appConfig, 'map.includeArchived', false),
|
||||
mapThemeMode<ThemeMode>(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)),
|
||||
mapWithPartners<bool>(.appConfig, 'map.withPartners', false),
|
||||
|
||||
// Cleanup
|
||||
cleanupKeepFavorites<bool>(.appConfig, 'cleanup.keepFavorites', true),
|
||||
cleanupKeepMediaType<AssetKeepType>(
|
||||
.appConfig,
|
||||
'cleanup.keepMediaType',
|
||||
AssetKeepType.none,
|
||||
_EnumCodec(AssetKeepType.values),
|
||||
),
|
||||
cleanupKeepAlbumIds<List<String>>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
|
||||
cleanupCutoffDaysAgo<int>(.appConfig, 'cleanup.cutoffDaysAgo', -1),
|
||||
cleanupDefaultsInitialized<bool>(.appConfig, 'cleanup.defaultsInitialized', false),
|
||||
|
||||
// Slideshow
|
||||
slideshowTransition<bool>(.appConfig, 'slideshow.transition', true),
|
||||
slideshowRepeat<bool>(.appConfig, 'slideshow.repeat', true),
|
||||
slideshowDuration<int>(.appConfig, 'slideshow.duration', 5),
|
||||
slideshowLook<SlideshowLook>(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)),
|
||||
slideshowDirection<SlideshowDirection>(
|
||||
.appConfig,
|
||||
'slideshow.direction',
|
||||
SlideshowDirection.forward,
|
||||
_EnumCodec(SlideshowDirection.values),
|
||||
);
|
||||
|
||||
final MetadataDomain domain;
|
||||
final String name;
|
||||
final T defaultValue;
|
||||
final _MetadataCodec<T>? _codecOverride;
|
||||
|
||||
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
|
||||
|
||||
String get key => '${domain.prefix}.$name';
|
||||
|
||||
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
|
||||
|
||||
String encode(T value) => _codec.encode(value);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
|
||||
|
||||
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
|
||||
}
|
||||
|
||||
sealed class _MetadataCodec<T extends Object> {
|
||||
const _MetadataCodec();
|
||||
|
||||
String encode(T value);
|
||||
T? decode(String raw);
|
||||
|
||||
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
||||
int: _PrimitiveCodec.integer,
|
||||
double: _PrimitiveCodec.real,
|
||||
bool: _PrimitiveCodec.boolean,
|
||||
String: _PrimitiveCodec.string,
|
||||
DateTime: _DateTimeCodec(),
|
||||
};
|
||||
|
||||
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
|
||||
final codec = _primitives[sample.runtimeType];
|
||||
if (codec == null) {
|
||||
throw StateError(
|
||||
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
|
||||
);
|
||||
}
|
||||
return codec as _MetadataCodec<T>;
|
||||
}
|
||||
}
|
||||
|
||||
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
|
||||
final List<T> values;
|
||||
|
||||
const _EnumCodec(this.values);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.name;
|
||||
|
||||
@override
|
||||
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
|
||||
}
|
||||
|
||||
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
||||
const _DateTimeCodec();
|
||||
|
||||
@override
|
||||
String encode(DateTime value) => value.toIso8601String();
|
||||
|
||||
@override
|
||||
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
||||
}
|
||||
|
||||
final class _MapCodec<K extends Object, V extends Object> extends _MetadataCodec<Map<K, V>> {
|
||||
final _MetadataCodec<K> _keyCodec;
|
||||
final _MetadataCodec<V> _valueCodec;
|
||||
|
||||
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||
|
||||
@override
|
||||
String encode(Map<K, V> value) {
|
||||
final entries = <String, String>{};
|
||||
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||
return jsonEncode(entries);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<K, V>? decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
return null;
|
||||
}
|
||||
final result = <K, V>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final rawKey = entry.key;
|
||||
final rawValue = entry.value;
|
||||
if (rawKey is! String || rawValue is! String) {
|
||||
return null;
|
||||
}
|
||||
final k = _keyCodec.decode(rawKey);
|
||||
final v = _valueCodec.decode(rawValue);
|
||||
if (k == null || v == null) {
|
||||
return null;
|
||||
}
|
||||
result[k] = v;
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _ListCodec<T extends Object> extends _MetadataCodec<List<T>> {
|
||||
final _MetadataCodec<T> _elementCodec;
|
||||
|
||||
const _ListCodec(this._elementCodec);
|
||||
|
||||
@override
|
||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||
|
||||
@override
|
||||
List<T>? decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return null;
|
||||
}
|
||||
final result = <T>[];
|
||||
for (final item in decoded) {
|
||||
if (item is! String) {
|
||||
return null;
|
||||
}
|
||||
final element = _elementCodec.decode(item);
|
||||
if (element == null) {
|
||||
return null;
|
||||
}
|
||||
result.add(element);
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
||||
final T? Function(String) _parse;
|
||||
|
||||
const _PrimitiveCodec._(this._parse);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.toString();
|
||||
|
||||
@override
|
||||
T? decode(String raw) => _parse(raw);
|
||||
|
||||
static const integer = _PrimitiveCodec<int>._(int.tryParse);
|
||||
static const real = _PrimitiveCodec<double>._(double.tryParse);
|
||||
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
|
||||
static const string = _PrimitiveCodec<String>._(_identity);
|
||||
|
||||
static String? _identity(String s) => s;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/colors.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
|
||||
enum SettingsKey<T extends Object> {
|
||||
// Theme
|
||||
themePrimaryColor<ImmichColorPreset>(codec: _EnumCodec(ImmichColorPreset.values)),
|
||||
themeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||
themeDynamic<bool>(),
|
||||
themeColorfulInterface<bool>(),
|
||||
|
||||
// Image
|
||||
imagePreferRemote<bool>(),
|
||||
imageLoadOriginal<bool>(),
|
||||
|
||||
// Viewer
|
||||
viewerLoopVideo<bool>(),
|
||||
viewerLoadOriginalVideo<bool>(),
|
||||
viewerAutoPlayVideo<bool>(),
|
||||
viewerTapToNavigate<bool>(),
|
||||
|
||||
// Network
|
||||
networkAutoEndpointSwitching<bool>(),
|
||||
networkPreferredWifiName<String>(),
|
||||
networkLocalEndpoint<String>(),
|
||||
networkExternalEndpointList<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
networkCustomHeaders<Map<String, String>>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)),
|
||||
|
||||
// Album
|
||||
albumSortMode<AlbumSortMode>(codec: _EnumCodec(AlbumSortMode.values)),
|
||||
albumIsReverse<bool>(),
|
||||
albumIsGrid<bool>(),
|
||||
|
||||
// Backup
|
||||
backupEnabled<bool>(),
|
||||
backupUseCellularForVideos<bool>(),
|
||||
backupUseCellularForPhotos<bool>(),
|
||||
backupRequireCharging<bool>(),
|
||||
backupTriggerDelay<int>(),
|
||||
backupSyncAlbums<bool>(),
|
||||
|
||||
// Timeline
|
||||
timelineTilesPerRow<int>(),
|
||||
timelineGroupAssetsBy<GroupAssetsBy>(codec: _EnumCodec(GroupAssetsBy.values)),
|
||||
timelineStorageIndicator<bool>(),
|
||||
|
||||
// Log
|
||||
logLevel<LogLevel>(codec: _EnumCodec(LogLevel.values)),
|
||||
|
||||
// Map
|
||||
mapShowFavoriteOnly<bool>(),
|
||||
mapRelativeDate<int>(),
|
||||
mapIncludeArchived<bool>(),
|
||||
mapThemeMode<ThemeMode>(codec: _EnumCodec(ThemeMode.values)),
|
||||
mapWithPartners<bool>(),
|
||||
|
||||
// Cleanup
|
||||
cleanupKeepFavorites<bool>(),
|
||||
cleanupKeepMediaType<AssetKeepType>(codec: _EnumCodec(AssetKeepType.values)),
|
||||
cleanupKeepAlbumIds<List<String>>(codec: _ListCodec(_PrimitiveCodec.string)),
|
||||
cleanupCutoffDaysAgo<int>(),
|
||||
cleanupDefaultsInitialized<bool>(),
|
||||
|
||||
// Slideshow
|
||||
slideshowTransition<bool>(),
|
||||
slideshowRepeat<bool>(),
|
||||
slideshowDuration<int>(),
|
||||
slideshowLook<SlideshowLook>(codec: _EnumCodec(SlideshowLook.values)),
|
||||
slideshowDirection<SlideshowDirection>(codec: _EnumCodec(SlideshowDirection.values));
|
||||
|
||||
final _SettingsCodec<T>? _codecOverride;
|
||||
|
||||
const SettingsKey({_SettingsCodec<T>? codec}) : _codecOverride = codec;
|
||||
|
||||
_SettingsCodec<T> get _codec => _codecOverride ?? _SettingsCodec.forType(T);
|
||||
|
||||
String encode(T value) => _codec.encode(value);
|
||||
|
||||
T decode(String raw) => _codec.decode(raw);
|
||||
}
|
||||
|
||||
sealed class _SettingsCodec<T extends Object> {
|
||||
const _SettingsCodec();
|
||||
|
||||
String encode(T value);
|
||||
T decode(String raw);
|
||||
|
||||
static const Map<Type, _SettingsCodec<Object>> _primitives = {
|
||||
int: _PrimitiveCodec.integer,
|
||||
double: _PrimitiveCodec.real,
|
||||
bool: _PrimitiveCodec.boolean,
|
||||
String: _PrimitiveCodec.string,
|
||||
DateTime: _DateTimeCodec(),
|
||||
};
|
||||
|
||||
static _SettingsCodec<T> forType<T extends Object>(Type runtimeType) {
|
||||
final codec = _primitives[runtimeType];
|
||||
if (codec == null) {
|
||||
throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.');
|
||||
}
|
||||
return codec as _SettingsCodec<T>;
|
||||
}
|
||||
}
|
||||
|
||||
final class _EnumCodec<T extends Enum> extends _SettingsCodec<T> {
|
||||
final List<T> values;
|
||||
|
||||
const _EnumCodec(this.values);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.name;
|
||||
|
||||
@override
|
||||
T decode(String raw) => values.firstWhere((v) => v.name == raw);
|
||||
}
|
||||
|
||||
final class _DateTimeCodec extends _SettingsCodec<DateTime> {
|
||||
const _DateTimeCodec();
|
||||
|
||||
@override
|
||||
String encode(DateTime value) => value.toIso8601String();
|
||||
|
||||
@override
|
||||
DateTime decode(String raw) => DateTime.parse(raw);
|
||||
}
|
||||
|
||||
final class _MapCodec<K extends Object, V extends Object> extends _SettingsCodec<Map<K, V>> {
|
||||
final _SettingsCodec<K> _keyCodec;
|
||||
final _SettingsCodec<V> _valueCodec;
|
||||
|
||||
const _MapCodec(this._keyCodec, this._valueCodec);
|
||||
|
||||
@override
|
||||
String encode(Map<K, V> value) {
|
||||
final entries = <String, String>{};
|
||||
value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v));
|
||||
return jsonEncode(entries);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<K, V> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
return {};
|
||||
}
|
||||
final result = <K, V>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final rawKey = entry.key;
|
||||
final rawValue = entry.value;
|
||||
if (rawKey is! String || rawValue is! String) {
|
||||
return {};
|
||||
}
|
||||
final k = _keyCodec.decode(rawKey);
|
||||
final v = _valueCodec.decode(rawValue);
|
||||
result[k] = v;
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _ListCodec<T extends Object> extends _SettingsCodec<List<T>> {
|
||||
final _SettingsCodec<T> _elementCodec;
|
||||
|
||||
const _ListCodec(this._elementCodec);
|
||||
|
||||
@override
|
||||
String encode(List<T> value) => jsonEncode(value.map(_elementCodec.encode).toList());
|
||||
|
||||
@override
|
||||
List<T> decode(String raw) {
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
return [];
|
||||
}
|
||||
final result = <T>[];
|
||||
for (final item in decoded) {
|
||||
if (item is! String) {
|
||||
return [];
|
||||
}
|
||||
final element = _elementCodec.decode(item);
|
||||
result.add(element);
|
||||
}
|
||||
return result;
|
||||
} on FormatException {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _PrimitiveCodec<T extends Object> extends _SettingsCodec<T> {
|
||||
final T Function(String) _parse;
|
||||
|
||||
const _PrimitiveCodec._(this._parse);
|
||||
|
||||
@override
|
||||
String encode(T value) => value.toString();
|
||||
|
||||
@override
|
||||
T decode(String raw) => _parse(raw);
|
||||
|
||||
static const integer = _PrimitiveCodec<int>._(int.parse);
|
||||
static const real = _PrimitiveCodec<double>._(double.parse);
|
||||
static const boolean = _PrimitiveCodec<bool>._(bool.parse);
|
||||
static const string = _PrimitiveCodec<String>._(_identity);
|
||||
|
||||
static String _identity(String s) => s;
|
||||
}
|
||||
@@ -18,8 +18,6 @@ enum StoreKey<T> {
|
||||
|
||||
syncMigrationStatus<String>._(1013),
|
||||
|
||||
reviewOutOfSyncChangesAndroid<bool>._(1014),
|
||||
|
||||
// Legacy keys that have been migrated to the new metadata store
|
||||
legacyBackupRequireCharging<bool>._(7),
|
||||
legacyBackupTriggerDelay<int>._(8),
|
||||
|
||||
@@ -8,11 +8,7 @@ class AssetService {
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
|
||||
const AssetService({
|
||||
required RemoteAssetRepository remoteAssetRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
}) : _remoteAssetRepository = remoteAssetRepository,
|
||||
_localAssetRepository = localAssetRepository;
|
||||
const AssetService({required this._remoteAssetRepository, required this._localAssetRepository});
|
||||
|
||||
Future<BaseAsset?> getAsset(BaseAsset asset) {
|
||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
@@ -39,7 +39,7 @@ class BackgroundWorkerFgService {
|
||||
_foregroundHostApi.saveNotificationMessage(title, body);
|
||||
|
||||
Future<void> configure({int? minimumDelaySeconds, bool? requireCharging}) {
|
||||
final backup = MetadataRepository.instance.appConfig.backup;
|
||||
final backup = SettingsRepository.instance.appConfig.backup;
|
||||
return _foregroundHostApi.configure(
|
||||
BackgroundWorkerSettings(
|
||||
minimumDelaySeconds: minimumDelaySeconds ?? backup.triggerDelay,
|
||||
@@ -61,15 +61,13 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
|
||||
: _drift = drift,
|
||||
_driftLogger = driftLogger,
|
||||
_backgroundHostApi = BackgroundWorkerBgHostApi() {
|
||||
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
|
||||
BackgroundWorkerBgService({required this._drift, required this._driftLogger})
|
||||
: _backgroundHostApi = BackgroundWorkerBgHostApi() {
|
||||
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(_drift))]);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
bool get _isBackupEnabled => MetadataRepository.instance.appConfig.backup.enabled;
|
||||
bool get _isBackupEnabled => SettingsRepository.instance.appConfig.backup.enabled;
|
||||
|
||||
Future<void> init() async {
|
||||
try {
|
||||
|
||||
@@ -21,18 +21,13 @@ class HashService {
|
||||
final _log = Logger('HashService');
|
||||
|
||||
HashService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
bool Function()? cancelChecker,
|
||||
required this._localAlbumRepository,
|
||||
required this._localAssetRepository,
|
||||
required this._trashedLocalAssetRepository,
|
||||
required this._nativeSyncApi,
|
||||
this._cancelChecker,
|
||||
int? batchSize,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_cancelChecker = cancelChecker,
|
||||
_nativeSyncApi = nativeSyncApi,
|
||||
_batchSize = batchSize ?? kBatchHashFileLimit;
|
||||
}) : _batchSize = batchSize ?? kBatchHashFileLimit;
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
|
||||
@@ -4,12 +4,15 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trash_sync.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/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/diff.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -20,29 +23,30 @@ class LocalSyncService {
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final DriftTrashSyncRepository _trashSyncRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
final Logger _log = Logger("DeviceSyncService");
|
||||
|
||||
LocalSyncService({
|
||||
required DriftLocalAlbumRepository localAlbumRepository,
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required DriftTrashedLocalAssetRepository trashedLocalAssetRepository,
|
||||
required DriftTrashSyncRepository trashSyncRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_trashedLocalAssetRepository = trashedLocalAssetRepository,
|
||||
_trashSyncRepository = trashSyncRepository,
|
||||
_nativeSyncApi = nativeSyncApi;
|
||||
required this._localAlbumRepository,
|
||||
required this._localAssetRepository,
|
||||
required this._nativeSyncApi,
|
||||
required this._trashedLocalAssetRepository,
|
||||
required this._assetMediaRepository,
|
||||
required this._permissionRepository,
|
||||
});
|
||||
|
||||
Future<void> sync({bool full = false}) async {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
try {
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
await _syncTrashedAssets();
|
||||
await _trashSyncRepository.syncRestoresForRevivedAssets();
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
final hasPermission = await _permissionRepository.hasManageMediaPermission();
|
||||
if (hasPermission) {
|
||||
await _syncTrashedAssets();
|
||||
} else {
|
||||
_log.warning("syncTrashedAssets cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
}
|
||||
}
|
||||
await _trashSyncRepository.recheckRemoteTrashCandidates();
|
||||
|
||||
if (CurrentPlatform.isIOS) {
|
||||
// final assets = await _localAssetRepository.getEmptyCloudIdAssets();
|
||||
@@ -51,11 +55,7 @@ class LocalSyncService {
|
||||
|
||||
if (full || await _nativeSyncApi.shouldFullSync()) {
|
||||
_log.fine("Full sync request from ${full ? "user" : "native"}");
|
||||
await fullSync();
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
await _cleanupTrashSync();
|
||||
}
|
||||
return;
|
||||
return await fullSync();
|
||||
}
|
||||
|
||||
final delta = await _nativeSyncApi.getMediaChanges();
|
||||
@@ -77,13 +77,13 @@ class LocalSyncService {
|
||||
);
|
||||
|
||||
final dbAlbums = await _localAlbumRepository.getAll();
|
||||
// On Android, we need to sync all albums since it is not possible to
|
||||
// detect album deletions from the native side
|
||||
if (CurrentPlatform.isAndroid) {
|
||||
for (final album in dbAlbums) {
|
||||
final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id);
|
||||
await _localAlbumRepository.syncDeletes(album.id, deviceIds);
|
||||
}
|
||||
|
||||
await _cleanupTrashSync();
|
||||
}
|
||||
|
||||
if (CurrentPlatform.isIOS) {
|
||||
@@ -110,13 +110,6 @@ class LocalSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _cleanupTrashSync() async {
|
||||
final deleted = await _trashSyncRepository.cleanup();
|
||||
if (deleted > 0) {
|
||||
_log.fine("cleanup TrashState, deleted: $deleted");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fullSync() async {
|
||||
try {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
@@ -364,7 +357,7 @@ class LocalSyncService {
|
||||
@visibleForTesting
|
||||
Future<void> processTrashedAssets(Map<String, List<PlatformAsset>> trashedAssetMap) async {
|
||||
if (trashedAssetMap.isEmpty) {
|
||||
_log.fine("syncTrashedAssets, No trashed assets found");
|
||||
_log.info("syncTrashedAssets, No trashed assets found");
|
||||
}
|
||||
final trashedAssets = trashedAssetMap.cast<String, List<Object?>>().entries.expand(
|
||||
(entry) => entry.value.cast<PlatformAsset>().toTrashedAssets(entry.key),
|
||||
@@ -372,6 +365,30 @@ class LocalSyncService {
|
||||
|
||||
_log.fine("syncTrashedAssets, trashedAssets: ${trashedAssets.map((e) => e.asset.id)}");
|
||||
await _trashedLocalAssetRepository.processTrashSnapshot(trashedAssets);
|
||||
|
||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||
if (assetsToRestore.isNotEmpty) {
|
||||
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||
} else {
|
||||
_log.info("syncTrashedAssets, No remote assets found for restoration");
|
||||
}
|
||||
|
||||
final localAssetsToTrash = await _trashedLocalAssetRepository.getToTrash();
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||
_log.info("Moving to trash ${localIds.join(", ")} assets");
|
||||
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||
if (movedIds.isNotEmpty) {
|
||||
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||
)..removeWhere((_, assets) => assets.isEmpty);
|
||||
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||
}
|
||||
} else {
|
||||
_log.info("syncTrashedAssets, No assets found in backup-enabled albums for move to trash");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +433,7 @@ extension PlatformToLocalAsset on PlatformAsset {
|
||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||
import 'package:immich_mobile/domain/models/settings_key.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -12,10 +12,10 @@ import 'package:logging/logging.dart';
|
||||
///
|
||||
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
|
||||
/// writes them to a persistent [LogRepository], and manages log levels via
|
||||
/// [MetadataRepository].
|
||||
/// [SettingsRepository].
|
||||
class LogService {
|
||||
final LogRepository _logRepository;
|
||||
final MetadataRepository _metadataRepository;
|
||||
final SettingsRepository _settingsRepository;
|
||||
|
||||
final List<LogMessage> _msgBuffer = [];
|
||||
|
||||
@@ -38,12 +38,12 @@ class LogService {
|
||||
|
||||
static Future<LogService> init({
|
||||
required LogRepository logRepository,
|
||||
required MetadataRepository metadataRepository,
|
||||
required SettingsRepository settingsRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
_instance ??= await create(
|
||||
logRepository: logRepository,
|
||||
metadataRepository: metadataRepository,
|
||||
settingsRepository: settingsRepository,
|
||||
shouldBuffer: shouldBuffer,
|
||||
);
|
||||
return _instance!;
|
||||
@@ -51,17 +51,17 @@ class LogService {
|
||||
|
||||
static Future<LogService> create({
|
||||
required LogRepository logRepository,
|
||||
required MetadataRepository metadataRepository,
|
||||
required SettingsRepository settingsRepository,
|
||||
bool shouldBuffer = true,
|
||||
}) async {
|
||||
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
||||
final instance = LogService._(logRepository, settingsRepository, shouldBuffer);
|
||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||
final level = instance._metadataRepository.systemConfig.logLevel;
|
||||
final level = instance._settingsRepository.appConfig.logLevel;
|
||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||
return instance;
|
||||
}
|
||||
|
||||
LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) {
|
||||
LogService._(this._logRepository, this._settingsRepository, this._shouldBuffer) {
|
||||
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class LogService {
|
||||
}
|
||||
|
||||
Future<void> setLogLevel(LogLevel level) async {
|
||||
await _metadataRepository.write(MetadataKey.logLevel, level);
|
||||
await _settingsRepository.write(SettingsKey.logLevel, level);
|
||||
Logger.root.level = level.toLevel();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ typedef MapQuery = ({MapMarkerSource markerSource});
|
||||
class MapFactory {
|
||||
final DriftMapRepository _mapRepository;
|
||||
|
||||
const MapFactory({required DriftMapRepository mapRepository}) : _mapRepository = mapRepository;
|
||||
const MapFactory({required this._mapRepository});
|
||||
|
||||
MapService remote(List<String> ownerIds, TimelineMapOptions options) =>
|
||||
MapService(_mapRepository.remote(ownerIds, options));
|
||||
|
||||
@@ -9,7 +9,7 @@ final AppSetting = SettingsService(storeService: StoreService.I);
|
||||
class SettingsService {
|
||||
final StoreService _storeService;
|
||||
|
||||
const SettingsService({required StoreService storeService}) : _storeService = storeService;
|
||||
const SettingsService({required this._storeService});
|
||||
|
||||
T get<T>(Setting<T> setting) => _storeService.get(setting.storeKey, setting.defaultValue);
|
||||
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/sync_event.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.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/sync_api.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/trash_sync.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.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/utils/semver.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -22,31 +27,30 @@ enum SyncMigrationTask {
|
||||
v20260597_ResetAssetV1AssetV2, // Assets didn't include the uploadedAt column.
|
||||
}
|
||||
|
||||
typedef _RemoteAssetTrashState = ({String id, DateTime? deletedAt, String? checksum});
|
||||
|
||||
class SyncStreamService {
|
||||
final Logger _logger = Logger('SyncStreamService');
|
||||
|
||||
final SyncApiRepository _syncApiRepository;
|
||||
final SyncStreamRepository _syncStreamRepository;
|
||||
final DriftTrashSyncRepository _trashSyncRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
final SyncMigrationRepository _syncMigrationRepository;
|
||||
final ApiService _api;
|
||||
final bool Function()? _cancelChecker;
|
||||
|
||||
SyncStreamService({
|
||||
required SyncApiRepository syncApiRepository,
|
||||
required SyncStreamRepository syncStreamRepository,
|
||||
required DriftTrashSyncRepository trashSyncRepository,
|
||||
required SyncMigrationRepository syncMigrationRepository,
|
||||
required ApiService api,
|
||||
bool Function()? cancelChecker,
|
||||
}) : _syncApiRepository = syncApiRepository,
|
||||
_syncStreamRepository = syncStreamRepository,
|
||||
_trashSyncRepository = trashSyncRepository,
|
||||
_syncMigrationRepository = syncMigrationRepository,
|
||||
_api = api,
|
||||
_cancelChecker = cancelChecker;
|
||||
required this._syncApiRepository,
|
||||
required this._syncStreamRepository,
|
||||
required this._localAssetRepository,
|
||||
required this._trashedLocalAssetRepository,
|
||||
required this._assetMediaRepository,
|
||||
required this._permissionRepository,
|
||||
required this._syncMigrationRepository,
|
||||
required this._api,
|
||||
this._cancelChecker,
|
||||
});
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
@@ -188,24 +192,22 @@ class SyncStreamService {
|
||||
case SyncEntityType.assetV1:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
|
||||
await _handleRemoteAssetTrashState(
|
||||
remoteSyncAssets.map((e) => (id: e.id, deletedAt: e.deletedAt, checksum: e.checksum)),
|
||||
);
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||
}
|
||||
return;
|
||||
case SyncEntityType.assetV2:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV2>();
|
||||
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
|
||||
await _handleRemoteAssetTrashState(
|
||||
remoteSyncAssets.map((e) => (id: e.id, deletedAt: e.deletedAt, checksum: e.checksum)),
|
||||
);
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||
}
|
||||
return;
|
||||
case SyncEntityType.assetDeleteV1:
|
||||
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
|
||||
final now = DateTime.now();
|
||||
final remoteDeletedAtByRemoteId = Map<String, DateTime>.fromEntries(
|
||||
remoteSyncAssets.map((e) => MapEntry(e.assetId, now)),
|
||||
);
|
||||
await _trashSyncRepository.recordRemoteTrash(remoteDeletedAtByRemoteId);
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
|
||||
}
|
||||
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
|
||||
case SyncEntityType.assetExifV1:
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||
@@ -476,17 +478,58 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRemoteAssetTrashState(Iterable<_RemoteAssetTrashState> remoteSyncAssets) async {
|
||||
final deleted = <String, DateTime>{};
|
||||
final aliveChecksums = <String>[];
|
||||
for (final e in remoteSyncAssets) {
|
||||
if (e.deletedAt != null) {
|
||||
deleted[e.id] = e.deletedAt!;
|
||||
} else if (e.checksum != null) {
|
||||
aliveChecksums.add(e.checksum!);
|
||||
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return Future.value();
|
||||
} else {
|
||||
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds);
|
||||
if (localAssetsToTrash.isNotEmpty) {
|
||||
await _trashLocalAssets(localAssetsToTrash);
|
||||
} else {
|
||||
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
|
||||
}
|
||||
}
|
||||
await _trashSyncRepository.recordRemoteTrash(deleted);
|
||||
await _trashSyncRepository.recordRemoteRestore(aliveChecksums);
|
||||
}
|
||||
|
||||
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
|
||||
final localIds = localAssetsToTrash.values.expand((assets) => assets).map((asset) => asset.id).toList();
|
||||
_logger.info("Moving to trash ${localIds.join(", ")} assets");
|
||||
final movedIds = await _assetMediaRepository.deleteAll(localIds);
|
||||
if (movedIds.isNotEmpty) {
|
||||
final movedAssetsByAlbum = localAssetsToTrash.map(
|
||||
(albumId, assets) => MapEntry(albumId, assets.where((asset) => movedIds.contains(asset.id)).toList()),
|
||||
)..removeWhere((_, assets) => assets.isEmpty);
|
||||
|
||||
await _trashedLocalAssetRepository.trashLocalAsset(movedAssetsByAlbum);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _applyRemoteRestoreToLocal() async {
|
||||
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
|
||||
if (assetsToRestore.isNotEmpty) {
|
||||
final restoredIds = await _assetMediaRepository.restoreAssetsFromTrash(assetsToRestore);
|
||||
await _trashedLocalAssetRepository.applyRestoredAssets(restoredIds);
|
||||
} else {
|
||||
_logger.info("No remote assets found for restoration");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
||||
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
await _handleRemoteDeleted(remoteIds);
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
||||
if (!(await _permissionRepository.hasManageMediaPermission())) {
|
||||
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
await _handleRemoteDeleted(remoteIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
|
||||
@@ -35,21 +35,16 @@ enum TimelineOrigin {
|
||||
albumActivities,
|
||||
folder,
|
||||
recentlyAdded,
|
||||
syncTrash,
|
||||
}
|
||||
|
||||
class TimelineFactory {
|
||||
final DriftTimelineRepository _timelineRepository;
|
||||
final MetadataRepository _metadataRepository;
|
||||
final SettingsRepository _settingsRepository;
|
||||
|
||||
const TimelineFactory({
|
||||
required DriftTimelineRepository timelineRepository,
|
||||
required MetadataRepository metadataRepository,
|
||||
}) : _timelineRepository = timelineRepository,
|
||||
_metadataRepository = metadataRepository;
|
||||
const TimelineFactory({required this._timelineRepository, required this._settingsRepository});
|
||||
|
||||
GroupAssetsBy get groupBy {
|
||||
final group = _metadataRepository.appConfig.timeline.groupAssetsBy;
|
||||
final group = _settingsRepository.appConfig.timeline.groupAssetsBy;
|
||||
// We do not support auto grouping in the new timeline yet, fallback to day grouping
|
||||
return group == GroupAssetsBy.auto ? GroupAssetsBy.day : group;
|
||||
}
|
||||
@@ -70,8 +65,6 @@ class TimelineFactory {
|
||||
|
||||
TimelineService trash(String userId) => TimelineService(_timelineRepository.trash(userId, groupBy));
|
||||
|
||||
TimelineService toTrashSyncReview() => TimelineService(_timelineRepository.toTrashSyncReview(groupBy));
|
||||
|
||||
TimelineService archive(String userId) => TimelineService(_timelineRepository.archived(userId, groupBy));
|
||||
|
||||
TimelineService lockedFolder(String userId) => TimelineService(_timelineRepository.locked(userId, groupBy));
|
||||
@@ -111,12 +104,7 @@ class TimelineService {
|
||||
TimelineService(TimelineQuery query)
|
||||
: this._(assetSource: query.assetSource, bucketSource: query.bucketSource, origin: query.origin);
|
||||
|
||||
TimelineService._({
|
||||
required TimelineAssetSource assetSource,
|
||||
required TimelineBucketSource bucketSource,
|
||||
required this.origin,
|
||||
}) : _assetSource = assetSource,
|
||||
_bucketSource = bucketSource {
|
||||
TimelineService._({required this._assetSource, required this._bucketSource, required this.origin}) {
|
||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||
_mutex.run(() async {
|
||||
final totalAssets = buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||
|
||||
@@ -12,9 +12,7 @@ class UserService {
|
||||
final UserApiRepository _userApiRepository;
|
||||
final StoreService _storeService;
|
||||
|
||||
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
|
||||
: _userApiRepository = userApiRepository,
|
||||
_storeService = storeService;
|
||||
UserService({required this._userApiRepository, required this._storeService});
|
||||
|
||||
UserDto getMyUser() {
|
||||
return _storeService.get(StoreKey.currentUser);
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
|
||||
class MetadataEntity extends Table with DriftDefaultsMixin {
|
||||
const MetadataEntity();
|
||||
class SettingsEntity extends Table with DriftDefaultsMixin {
|
||||
const SettingsEntity();
|
||||
|
||||
TextColumn get key => text()();
|
||||
|
||||
@@ -14,5 +14,5 @@ class MetadataEntity extends Table with DriftDefaultsMixin {
|
||||
Set<Column> get primaryKey => {key};
|
||||
|
||||
@override
|
||||
String get tableName => "metadata";
|
||||
String get tableName => "settings";
|
||||
}
|
||||
+74
-74
@@ -1,28 +1,28 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/settings.entity.dart'
|
||||
as i2;
|
||||
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||
|
||||
typedef $$MetadataEntityTableCreateCompanionBuilder =
|
||||
i1.MetadataEntityCompanion Function({
|
||||
typedef $$SettingsEntityTableCreateCompanionBuilder =
|
||||
i1.SettingsEntityCompanion Function({
|
||||
required String key,
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
typedef $$MetadataEntityTableUpdateCompanionBuilder =
|
||||
i1.MetadataEntityCompanion Function({
|
||||
typedef $$SettingsEntityTableUpdateCompanionBuilder =
|
||||
i1.SettingsEntityCompanion Function({
|
||||
i0.Value<String> key,
|
||||
i0.Value<String> value,
|
||||
i0.Value<DateTime> updatedAt,
|
||||
});
|
||||
|
||||
class $$MetadataEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableFilterComposer({
|
||||
class $$SettingsEntityTableFilterComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
|
||||
$$SettingsEntityTableFilterComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
@@ -45,9 +45,9 @@ class $$MetadataEntityTableFilterComposer
|
||||
);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableOrderingComposer({
|
||||
class $$SettingsEntityTableOrderingComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
|
||||
$$SettingsEntityTableOrderingComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
@@ -70,9 +70,9 @@ class $$MetadataEntityTableOrderingComposer
|
||||
);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$MetadataEntityTable> {
|
||||
$$MetadataEntityTableAnnotationComposer({
|
||||
class $$SettingsEntityTableAnnotationComposer
|
||||
extends i0.Composer<i0.GeneratedDatabase, i1.$SettingsEntityTable> {
|
||||
$$SettingsEntityTableAnnotationComposer({
|
||||
required super.$db,
|
||||
required super.$table,
|
||||
super.joinBuilder,
|
||||
@@ -89,47 +89,47 @@ class $$MetadataEntityTableAnnotationComposer
|
||||
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||
}
|
||||
|
||||
class $$MetadataEntityTableTableManager
|
||||
class $$SettingsEntityTableTableManager
|
||||
extends
|
||||
i0.RootTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData,
|
||||
i1.$$MetadataEntityTableFilterComposer,
|
||||
i1.$$MetadataEntityTableOrderingComposer,
|
||||
i1.$$MetadataEntityTableAnnotationComposer,
|
||||
$$MetadataEntityTableCreateCompanionBuilder,
|
||||
$$MetadataEntityTableUpdateCompanionBuilder,
|
||||
i1.$SettingsEntityTable,
|
||||
i1.SettingsEntityData,
|
||||
i1.$$SettingsEntityTableFilterComposer,
|
||||
i1.$$SettingsEntityTableOrderingComposer,
|
||||
i1.$$SettingsEntityTableAnnotationComposer,
|
||||
$$SettingsEntityTableCreateCompanionBuilder,
|
||||
$$SettingsEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.MetadataEntityData,
|
||||
i1.SettingsEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData
|
||||
i1.$SettingsEntityTable,
|
||||
i1.SettingsEntityData
|
||||
>,
|
||||
),
|
||||
i1.MetadataEntityData,
|
||||
i1.SettingsEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
> {
|
||||
$$MetadataEntityTableTableManager(
|
||||
$$SettingsEntityTableTableManager(
|
||||
i0.GeneratedDatabase db,
|
||||
i1.$MetadataEntityTable table,
|
||||
i1.$SettingsEntityTable table,
|
||||
) : super(
|
||||
i0.TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
createFilteringComposer: () =>
|
||||
i1.$$MetadataEntityTableFilterComposer($db: db, $table: table),
|
||||
i1.$$SettingsEntityTableFilterComposer($db: db, $table: table),
|
||||
createOrderingComposer: () =>
|
||||
i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table),
|
||||
i1.$$SettingsEntityTableOrderingComposer($db: db, $table: table),
|
||||
createComputedFieldComposer: () => i1
|
||||
.$$MetadataEntityTableAnnotationComposer($db: db, $table: table),
|
||||
.$$SettingsEntityTableAnnotationComposer($db: db, $table: table),
|
||||
updateCompanionCallback:
|
||||
({
|
||||
i0.Value<String> key = const i0.Value.absent(),
|
||||
i0.Value<String> value = const i0.Value.absent(),
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.MetadataEntityCompanion(
|
||||
}) => i1.SettingsEntityCompanion(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
@@ -139,7 +139,7 @@ class $$MetadataEntityTableTableManager
|
||||
required String key,
|
||||
required String value,
|
||||
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||
}) => i1.MetadataEntityCompanion.insert(
|
||||
}) => i1.SettingsEntityCompanion.insert(
|
||||
key: key,
|
||||
value: value,
|
||||
updatedAt: updatedAt,
|
||||
@@ -152,34 +152,34 @@ class $$MetadataEntityTableTableManager
|
||||
);
|
||||
}
|
||||
|
||||
typedef $$MetadataEntityTableProcessedTableManager =
|
||||
typedef $$SettingsEntityTableProcessedTableManager =
|
||||
i0.ProcessedTableManager<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData,
|
||||
i1.$$MetadataEntityTableFilterComposer,
|
||||
i1.$$MetadataEntityTableOrderingComposer,
|
||||
i1.$$MetadataEntityTableAnnotationComposer,
|
||||
$$MetadataEntityTableCreateCompanionBuilder,
|
||||
$$MetadataEntityTableUpdateCompanionBuilder,
|
||||
i1.$SettingsEntityTable,
|
||||
i1.SettingsEntityData,
|
||||
i1.$$SettingsEntityTableFilterComposer,
|
||||
i1.$$SettingsEntityTableOrderingComposer,
|
||||
i1.$$SettingsEntityTableAnnotationComposer,
|
||||
$$SettingsEntityTableCreateCompanionBuilder,
|
||||
$$SettingsEntityTableUpdateCompanionBuilder,
|
||||
(
|
||||
i1.MetadataEntityData,
|
||||
i1.SettingsEntityData,
|
||||
i0.BaseReferences<
|
||||
i0.GeneratedDatabase,
|
||||
i1.$MetadataEntityTable,
|
||||
i1.MetadataEntityData
|
||||
i1.$SettingsEntityTable,
|
||||
i1.SettingsEntityData
|
||||
>,
|
||||
),
|
||||
i1.MetadataEntityData,
|
||||
i1.SettingsEntityData,
|
||||
i0.PrefetchHooks Function()
|
||||
>;
|
||||
|
||||
class $MetadataEntityTable extends i2.MetadataEntity
|
||||
with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> {
|
||||
class $SettingsEntityTable extends i2.SettingsEntity
|
||||
with i0.TableInfo<$SettingsEntityTable, i1.SettingsEntityData> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$MetadataEntityTable(this.attachedDatabase, [this._alias]);
|
||||
$SettingsEntityTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> key = i0.GeneratedColumn<String>(
|
||||
@@ -219,10 +219,10 @@ class $MetadataEntityTable extends i2.MetadataEntity
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'metadata';
|
||||
static const String $name = 'settings';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(
|
||||
i0.Insertable<i1.MetadataEntityData> instance, {
|
||||
i0.Insertable<i1.SettingsEntityData> instance, {
|
||||
bool isInserting = false,
|
||||
}) {
|
||||
final context = i0.VerificationContext();
|
||||
@@ -255,9 +255,9 @@ class $MetadataEntityTable extends i2.MetadataEntity
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {key};
|
||||
@override
|
||||
i1.MetadataEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
i1.SettingsEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.MetadataEntityData(
|
||||
return i1.SettingsEntityData(
|
||||
key: attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.string,
|
||||
data['${effectivePrefix}key'],
|
||||
@@ -274,8 +274,8 @@ class $MetadataEntityTable extends i2.MetadataEntity
|
||||
}
|
||||
|
||||
@override
|
||||
$MetadataEntityTable createAlias(String alias) {
|
||||
return $MetadataEntityTable(attachedDatabase, alias);
|
||||
$SettingsEntityTable createAlias(String alias) {
|
||||
return $SettingsEntityTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -284,12 +284,12 @@ class $MetadataEntityTable extends i2.MetadataEntity
|
||||
bool get isStrict => true;
|
||||
}
|
||||
|
||||
class MetadataEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.MetadataEntityData> {
|
||||
class SettingsEntityData extends i0.DataClass
|
||||
implements i0.Insertable<i1.SettingsEntityData> {
|
||||
final String key;
|
||||
final String value;
|
||||
final DateTime updatedAt;
|
||||
const MetadataEntityData({
|
||||
const SettingsEntityData({
|
||||
required this.key,
|
||||
required this.value,
|
||||
required this.updatedAt,
|
||||
@@ -303,12 +303,12 @@ class MetadataEntityData extends i0.DataClass
|
||||
return map;
|
||||
}
|
||||
|
||||
factory MetadataEntityData.fromJson(
|
||||
factory SettingsEntityData.fromJson(
|
||||
Map<String, dynamic> json, {
|
||||
i0.ValueSerializer? serializer,
|
||||
}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return MetadataEntityData(
|
||||
return SettingsEntityData(
|
||||
key: serializer.fromJson<String>(json['key']),
|
||||
value: serializer.fromJson<String>(json['value']),
|
||||
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||
@@ -324,17 +324,17 @@ class MetadataEntityData extends i0.DataClass
|
||||
};
|
||||
}
|
||||
|
||||
i1.MetadataEntityData copyWith({
|
||||
i1.SettingsEntityData copyWith({
|
||||
String? key,
|
||||
String? value,
|
||||
DateTime? updatedAt,
|
||||
}) => i1.MetadataEntityData(
|
||||
}) => i1.SettingsEntityData(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) {
|
||||
return MetadataEntityData(
|
||||
SettingsEntityData copyWithCompanion(i1.SettingsEntityCompanion data) {
|
||||
return SettingsEntityData(
|
||||
key: data.key.present ? data.key.value : this.key,
|
||||
value: data.value.present ? data.value.value : this.value,
|
||||
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||
@@ -343,7 +343,7 @@ class MetadataEntityData extends i0.DataClass
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MetadataEntityData(')
|
||||
return (StringBuffer('SettingsEntityData(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
@@ -356,29 +356,29 @@ class MetadataEntityData extends i0.DataClass
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.MetadataEntityData &&
|
||||
(other is i1.SettingsEntityData &&
|
||||
other.key == this.key &&
|
||||
other.value == this.value &&
|
||||
other.updatedAt == this.updatedAt);
|
||||
}
|
||||
|
||||
class MetadataEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.MetadataEntityData> {
|
||||
class SettingsEntityCompanion
|
||||
extends i0.UpdateCompanion<i1.SettingsEntityData> {
|
||||
final i0.Value<String> key;
|
||||
final i0.Value<String> value;
|
||||
final i0.Value<DateTime> updatedAt;
|
||||
const MetadataEntityCompanion({
|
||||
const SettingsEntityCompanion({
|
||||
this.key = const i0.Value.absent(),
|
||||
this.value = const i0.Value.absent(),
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
});
|
||||
MetadataEntityCompanion.insert({
|
||||
SettingsEntityCompanion.insert({
|
||||
required String key,
|
||||
required String value,
|
||||
this.updatedAt = const i0.Value.absent(),
|
||||
}) : key = i0.Value(key),
|
||||
value = i0.Value(value);
|
||||
static i0.Insertable<i1.MetadataEntityData> custom({
|
||||
static i0.Insertable<i1.SettingsEntityData> custom({
|
||||
i0.Expression<String>? key,
|
||||
i0.Expression<String>? value,
|
||||
i0.Expression<DateTime>? updatedAt,
|
||||
@@ -390,12 +390,12 @@ class MetadataEntityCompanion
|
||||
});
|
||||
}
|
||||
|
||||
i1.MetadataEntityCompanion copyWith({
|
||||
i1.SettingsEntityCompanion copyWith({
|
||||
i0.Value<String>? key,
|
||||
i0.Value<String>? value,
|
||||
i0.Value<DateTime>? updatedAt,
|
||||
}) {
|
||||
return i1.MetadataEntityCompanion(
|
||||
return i1.SettingsEntityCompanion(
|
||||
key: key ?? this.key,
|
||||
value: value ?? this.value,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
@@ -419,7 +419,7 @@ class MetadataEntityCompanion
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('MetadataEntityCompanion(')
|
||||
return (StringBuffer('SettingsEntityCompanion(')
|
||||
..write('key: $key, ')
|
||||
..write('value: $value, ')
|
||||
..write('updatedAt: $updatedAt')
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trash_sync.entity.drift.dart';
|
||||
|
||||
enum TrashStateDecision {
|
||||
// do not change this order!
|
||||
pendingReview,
|
||||
kept,
|
||||
appTrashed,
|
||||
}
|
||||
|
||||
enum TrashTriggerSource {
|
||||
// do not change this order!
|
||||
remoteSync,
|
||||
localUser,
|
||||
}
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trash_sync_decision ON trash_sync_entity (decision)')
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_trash_sync_checksum ON trash_sync_entity (checksum)')
|
||||
class TrashSyncEntity extends LocalAssetEntity {
|
||||
const TrashSyncEntity();
|
||||
|
||||
IntColumn get decision => intEnum<TrashStateDecision>()();
|
||||
|
||||
IntColumn get triggerSource => intEnum<TrashTriggerSource>()();
|
||||
|
||||
DateTimeColumn get remoteDeletedAt => dateTime().nullable()();
|
||||
|
||||
DateTimeColumn get decidedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
extension TrashSyncEntityDataDomainExtension on TrashSyncEntityData {
|
||||
LocalAsset toLocalAsset() => LocalAsset(
|
||||
id: id,
|
||||
name: name,
|
||||
checksum: checksum,
|
||||
type: type,
|
||||
createdAt: createdAt,
|
||||
updatedAt: updatedAt,
|
||||
durationMs: durationMs,
|
||||
isFavorite: isFavorite,
|
||||
height: height,
|
||||
width: width,
|
||||
orientation: orientation,
|
||||
playbackStyle: playbackStyle,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
cloudId: iCloudId,
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user